1.先看下最终效果react
当咱们发起一个POST /users的请求指望新增一个用户git
@PostMapping("/users") public void addUser(@Valid @RequestBody User user) { log.info("用户添加成功:{}", user); }
假设携带如下JSON数据做为请求参数,可是一般咱们会指望username和password不能为空web
{ "username":"", "password":"" }
所以咱们指望能获得一个具体的响应,告诉咱们参数校验失败的个数及缘由正则表达式
{ "code": 400, "message": "BAD_REQUEST", "data": "参数校验错误(2):用户名不能为空;密码不能为空" }
data中的(2) 表示有两处参数校验失败并在其后代表校验失败的缘由spring
2.接下来讲明下实现过程数组
总共分为三步app
第一步,在参数上添加声明式注解定义参数须要的校验类型,例如上文的User对象ide
@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格式 |
注意:spring-boot
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>