java后台数据校验 JSR303规范
JSR303
是一种用于检查数据完整性的标准,javax.validation.constraints
提供了一部分实现
使用方法:
- 在 bean 中添加约束注解,并添加message
- 使用 bean 的地方使用
@Valid
注解开启校验 - 在要校验的目标参数后紧跟一个
BindingResult
就可以获得校验的结果,其位于org.springframework.validation.BindingResult
@Data
public class Brand {
private Long brandId;
@NotBlank(message = "品牌名不能为空") // 用于校验的注解
private String name;
}
@PostMapping
public Result save(@Valid @RequestBody Brand brand, BindingResult result){
// 通过BindingResult,获取校验结果
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
result.getFieldErrors().forEach((fieldError) -> {
String field = fieldError.getField();
String message = fieldError.getDefaultMessage();
map.put(field, message);
});
Result<Map> error = new Result<Map>().error(400, "提交数据不合法");
error.setData(map);
return error;
}
brandService.save(brand);
return new Result();
}
统一异常处理
使用 @ControllerAdvice
,在 controller 中出现数据校验错误时不在自行处理,而是抛出异常,由异常处理类统一处理。在controller中不接收 BindingResult 异常就会抛出。
@Slf4j
@RestControllerAdvice(basePackages = "org.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
// 指处理方法参数错误的特定异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<Map> handleVaildException(MethodArgumentNotValidException e) {
log.error("数据校验错误 {} {}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
map.put(fieldError.getField(), fieldError.getDefaultMessage());
});
Result<Map> error = new Result<Map>()
// 将状态码与错误消息封装到一个枚举类中
.error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMsg());
error.setData(map);
return error;
}
// 处理所有异常
@ExceptionHandler
public Result handleException(Throwable throwable) {
Result<Object> result = new Result<>()
.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
result.setData(throwable);
return result;
}
}
分组校验
当新增一个对象时自动生成id,故无需携带id;修改对象时必须提交对象的id。但是校验条件注解都是在实体类中定义。
解决方法:注解有一个 groups 属性。通过这个属性指定不同组,从而实现在不同条件下使用不同的约束。
然后再在数据校验处指定分组
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}"; // 这条消息默认从 validation.properties 文件中获取,可以在自己的项目中自定义
Class<?>[] groups() default {}; // 通过实现的接口进行分组,接口不需要有方法
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
1. 创建用于区分场景的接口
// 空接口即可,无需实现
package org.gulimall.common.valid;
public interface AddGroup {
}
package org.gulimall.common.valid;
public interface UpdateGroup {
}
2. 在实体中注明分组
通过分组能够实现不同的策略。注意:在使用分组的情况下,不指定分组的属性校验注解默认不起作用。
@Data
public class BrandDTO {
@NotNull(message = "修改时id不能为空", groups = {UpdateGroup.class})
@Null(message = "新增时id必须为空", groups = {AddGroup.class})
private Long brandId;
}
3. 在数据校验处指定分组
通过@Validated
指定分组。注意:在使用分组的情况下,不指定分组的属性校验注解默认不起作用。
@RestController
@RequestMapping("product/brand")
@Api(tags="品牌")
public class BrandController {
@Autowired
private BrandService brandService;
@PostMapping
public Result save(@Validated({AddGroup.class}) @RequestBody Brand brand){
brandService.save(brand);
return new Result();
}
@PutMapping
public Result update(@Validated({UpdateGroup.class}) @RequestBody Brand brand){
brandService.update(dto);
return new Result();
}
}
自定义校验
- 编写一个自定义的注解
- 编写一个自定义的校验器
- 二者关联
1. 参考 @NotNULL 写出一个自定义校验器
package org.gulimall.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = { StatusValueConstraintValidator.class }) // 指定具体的校验类,将二者关联。可以指定多个校验器,适配不同属性
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//@Repeatable(List.class)
public @interface StatusValue {
String message() default "{org.gulimall.common.valid.StatusValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
其中校验类validatedBy要求是指定类型子类的数组
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
/**
* {@link ConstraintValidator} classes implementing the constraint. The given classes
* must reference distinct target types for a given {@link ValidationTarget}. If two
* {@code ConstraintValidator}s refer to the same type, an exception will occur.
* <p>
* At most one {@code ConstraintValidator} targeting the array of parameters of
* methods or constructors (aka cross-parameter) is accepted. If two or more
* are present, an exception will occur.
*
* @return array of {@code ConstraintValidator} classes implementing the constraint
*/
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
2. 编写一个自定义的校验器,要实现ConstraintValidator<注解类型, 修饰的属性类型>
这里写的校验器只能处理int类型数据,如果要处理其他类型,新建一个校验器再添加到修饰注解的数组中
package org.gulimall.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* 要求实现 ConstraintValidator 接口,要有两个泛型参数,一个是注解类型,另一个是注解修饰的类型
*/
public class StatusValueConstraintValidator implements ConstraintValidator<StatusValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法,可以获得初始化信息
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
@Override
public void initialize(StatusValue constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
for (int val : constraintAnnotation.vals()) {
set.add(val);
}
}
/**
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*
* @return 是否通过校验
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
3. 在注解的注解中将二者关联
使用:
@Data
public class Brand {
// @Max(value = 1, message = "显示状态只能为0或1", groups = {UpdateGroup.class, AddGroup.class})
// @Min(value = 0, message = "显示状态只能为0或1", groups = {UpdateGroup.class, AddGroup.class})
@StatusValue(vals={0, 1}, groups = {UpdateGroup.class, AddGroup.class}) // 自定义注解,属性值只能为指定的 0 或 1
@ApiModelProperty(value = "显示状态[0-不显示;1-显示]")
private Integer showStatus;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现