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格式 |
注意:
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>
注:轉載https://my.oschina.net/u/3773384/blog/1795869