Java中JSR303校验
1、简介
jsr 是 Java Specification Requests 的缩写,意思是java的请求规范。周志明老师的书上还着重介绍过jsr292(jvm多语言支持包括Kotlin,Clojure,JRuby,Scala等)。
JSR303着重参数校验功能,点开javax.validation.constraints,可以看到已经封装好的注解有这些:
使用jsr303规范很简单,第一步在实体类相应字段上标注校验注解,比如@Email或者标注自定义校验注解@Pattern(regexp=”“)自定义正则表达式来处理;第二步是使用校验,只需要在@RequestBody之前加上@Valid注解即表明开启校验。
2、分组校验
更复杂的场景,我们可以分组校验:
1)、 @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class}) 给校验注解标注什么情况需要进行校验,比如增加不校验修改时校验。这里group里传入的是个接口。 2)、开启分组校验要使用spring实现的注解@Validated({AddGroup.class}) 3)、默认没有指定分组的校验注解比如@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效,也就是说Validated后加了分组那么不加分组的校验注解就会失效;
细节如下:
@Data @TableName("pms_brand") public class BrandEntity implements Serializable { private static final long serialVersionUID = 1L; /** * 品牌id */ @NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class}) @Null(message = "新增不能指定id", groups = {AddGroup.class}) @TableId private Long brandId; /** * 品牌名 */ @NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class}) private String name; /** * 品牌logo地址 */ @NotBlank(groups = {AddGroup.class}) @URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class}) private String logo; /** * 介绍 */ private String descript; /** * 显示状态[0-不显示;1-显示] */ // @Pattern() @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class}) @ListValue(values = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class}) private Integer showStatus; /** * 检索首字母 */ @NotEmpty(groups = {AddGroup.class}) @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class}) private String firstLetter; /** * 排序 */ @NotNull(groups = {AddGroup.class}) @Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class}) private Integer sort; } |
controller层内容:
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand) |
3、自定义校验注解
还有自定义校验,可以编写一个自定义的校验注解,然后编写一个自定义的校验器 ConstraintValidator,然后两者关联。
也就是说@Pattern()正则不能满足校验的情况,可以使用自定义校验注解。
比如对showStatus做自定义校验,规定只能是整数0或1。
需要先按照规范自定义一个注解@ListValue()
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Documented @Constraint(validatedBy = {ListValueConstraintValidator.class}) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) public @interface ListValue { String message() default "{com.flitsneak.common.valid.ListValue.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int[] values() default {}; } |
注解增加属性int[],提示信息仿照规范自定义common模块resource目录下新建
ValidationMessages.properties文件
com.flitsneak.common.valid.ListValue.message=必须提交指定的值 |
validatedBy后传入我们自定义的校验器,注解作为参数通过自定义校验器ListValueConstraintValidator对参数进行校验。
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; import java.util.Set; /** * @Author FlitSneak * @Date 2021/6/24 */ public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> { private final Set<Integer> set = new HashSet<>(); /** * 初始化方法 * 参数:自定义注解的详细信息 */ @Override public void initialize(ListValue constraintAnnotation) { int[] values = constraintAnnotation.values(); for (int val : values) { set.add(val); } } /** * 判断是否校验成功 * * @param value 需要校验的值 * @param context * @return */ @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } } |
就是将注解的值放大set集合之中,然后对字段之判断是否在set集合之中。
4、统一拦截处理
校验响应的结果可以用BindingResult来接收处理返回,但是相当麻烦,推荐做统一拦截处理。
BingdingResult处理方式:
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult){ if (bindingResult.hasErrors()){ Map<String,String> map = new HashMap<>(); //获取校验错误结果 bindingResult.getFieldErrors().forEach(i->{ //获取到错误提示 String message = i.getField(); //获取出错的字段 String field = i.getField(); map.put(field,message); }); return R.error(400,"校验错误").put("data",map); }else { brandService.save(brand); return R.ok();} } |
改为ControllerAdvice统一拦截处理,
@Slf4j @RestControllerAdvice(basePackages = "com.flitsneak.mall.product.controller") public class MallControllerAdvice { @ExceptionHandler(value= MethodArgumentNotValidException.class) public R handleValidException(MethodArgumentNotValidException e){ log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String,String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError)->{ errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",errorMap); } @ExceptionHandler(value = Throwable.class) public R handleException(Throwable throwable){ log.error("错误:",throwable); return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),BizCodeEnum.UNKNOWN_EXCEPTION.getMsg()); } } |
指定拦截的是MethodArgumentNotValidException异常,异常对象e里面可以获取BindingResult,处理方式一样。
补充
前端和后端都应该对参数做检验屏蔽非法请求,jsr303很多企业并没有应用,仍然是 使用CollectionUtil或者StringUtil进行处理。
我们项目用的springboot版本是2.3.x,而2.3.x以上剥离了jsr303,所以要使用注解校验,需要导入一下两个包:
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.3.4.RELEASE</version> </dependency> |
spring-boot-starter-validation下的version字段和springboot版本对应,本项目springboot版本是2.3.4.RELEASE