SpringBoot 中使用声明式注解简化参数校验

Java框架

浏览数:52

2020-6-1

1.先看下最终效果

当我们发起一个POST /users的请求期望新增一个用户

@PostMapping("/users")
public void addUser(@Valid @RequestBody User user) {
    log.info("用户添加成功:{}", user);
}

假设携带以下JSON数据作为请求参数,但是通常我们会期望username和password不能为空

{
        "username":"",
        "password":""
}

因此我们期望能得到一个具体的响应,告诉我们参数校验失败的个数及原因

{
    "code": 400,
    "message": "BAD_REQUEST",
    "data": "参数校验错误(2):用户名不能为空;密码不能为空"
}

data中的(2) 表示有两处参数校验失败并在其后表明校验失败的原因

 

2.接下来说明下实现过程

总共分为三步

 

第一步,在参数上添加声明式注解定义参数需要的校验类型,例如上文的User对象

@Data
public class User {
    private String id;
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
    @Email(message = "Email格式错误")
    private String email;
    @PastOrPresent(message = "日期小于等于当前时间")
    private Date birthday;
    @Pattern(regexp = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$"
            , message = "手机号格式错误")
    private String phone;
    @Min(value = 0, message = "年龄超出范围,最小值为0")
    @Max(value = 120, message = "年龄超出范围,最大值为120")
    private Integer age;
}
注解 描述
@AssertFalse 所注解的元素必须是Boolean类型,且值为false
@AssertTrue 所注解的元素必须是Boolean类型,且值为true
@DecimalMax 所注解的元素必须是数字,且值小于等于给定的值
@DecimalMin 所注解的元素必须是数字,且值大于等于给定的值
@Digits 所注解的元素必须是数字,且值必须是指定的位数
@Future 所注解的元素必须是将来某个日期
@Max 所注解的元素必须是数字,且值小于等于给定的值
@Min 所注解的元素必须是数字,且值小于等于给定的值
@Range 所注解的元素需在指定范围区间内
@NotNull 所注解的元素值不能为null
@NotBlank 所注解的元素值有内容
@Null 所注解的元素值为null
@Past 所注解的元素必须是某个过去的日期
@PastOrPresent 所注解的元素必须是过去某个或现在日期
@Pattern 所注解的元素必须满足给定的正则表达式
@Size 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
@Email 所注解的元素需满足Email格式

注意:

a.其中 ,username与password属性必传而其他属性没有限制

b.注解中的message属性会在校验失败抛出异常时赋给defaultMessage属性

 

第二步,在需要校验的参数前添加@Valid注解

public void addUser(@Valid @RequestBody User user)

注意

a.如果没有添加@Valid注解是不会对参数进行校验的

 

第三步,添加WebExchangeBindException异常处理器

@Slf4j
@RestControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(WebExchangeBindException.class)
    public Map<String, Object> handle(WebExchangeBindException exception) {
        //获取参数校验错误集合
        List<FieldError> fieldErrors = exception.getFieldErrors();
        //格式化以提供友好的错误提示
        String data = String.format("参数校验错误(%s):%s", fieldErrors.size(),
                fieldErrors.stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(";")));
        //参数校验失败响应失败个数及原因
        return ImmutableMap.of("code", exception.getStatus().value(),
                "message", exception.getStatus(),
                "data", data);
    }
}

@RestControllerAdvice @ResponseBody与@ControllerAdvice两个注解结合,表示当前类是一个控制器增强类,通常与@ExceptionHandler注解搭配来捕获处理异常

@ResponseBody 表示响应客户端时使用消息转换器(Message conversion)而不是内容协商(Content negotiation),默认使用Jackson解析,注解在类上表名类下的所有方法都需要响应为JSON(没有使用其他消息转换器的情况下)

@ExceptionHandler 注解的方法将会捕获并处理指定的异常,文中处理的是WebExchangeBindException异常

ImmutableMap.of() Guava提供的API,需要引入以下依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

 

以上,即可达到本文开头的效果啦。但是仍然存在可以改进的地方,例如,User对象的age属性的校验注解可以简化为

@Range(min = 0, max = 120, message = "年龄大于等于0小于等于120")
private Integer age;

同时,我们可以使用自定义参数校验器来实现一个通用的手机号码校验注解。

 

3.实现自定义的参数校验注解与校验器

上文中我们可以发现想要校验手机号码是否符合格式,需要在注解上添加一长串的正则表达式,下面让我们使用自定义的参数校验注解加上自定义的参数校验器来实现一个通用的手机号码校验规则。同样,分为三步

 

第一步,定义一个参数校验注解,复制@NotNull注解并加以修改

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//定义当前注解使用哪个参数校验器进行校验
@Constraint(validatedBy = PhoneValidator.class)
@Repeatable(Phone.List.class)
public @interface Phone {
    String message() default "手机号码格式错误";

    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
    @interface List {
        Phone[] value();
    }
}

注意:

a.message、groups、payload属性都需要定义在参数校验注解中不能缺省\

b.@Repeatable是JDK1.8中的元注解,表示在同一个位置重复相同的注解

如果使用的JDK版本低于1.8在可以使用以下方式创建@Phone注解

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号码格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

 

接着,需要定义上文的PhoneValidator参数校验器

public class PhoneValidator implements ConstraintValidator<Phone, Object> {
    private static final String PHONE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$";

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        //值不为空或者满足正则表达式时返回true
        return Objects.isNull(value) || Pattern.compile(PHONE_REGEX).matcher(value.toString()).find();
    }
}

 

最后,使用参数校验注解,替换上文中繁琐的@Pattern注解

@Phone
private String phone;

 

测试

 

使用自定义参数校验的优势

1.消除耦合,如果哪天你需要更改正则表达式你需要在每个引用的地方进行更改

2.通俗易懂,可以一目了然表示这是一个手机校验的注解,即便代码的阅读者对正则表达式不太熟悉,也可以猜出这个注解用来干嘛的

3.更强大,自定义校验逻辑,可以干更多的事情,甚至可以在校验器中引入其他的组件如使用@Autowired引入服务类进行处理校验判断

 

最后贴出本文的pom.xml文件

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

作者:秋田君