Spring Validation 的使用
- zhangxs
- 2019-4-14
使用背景
目前在新樂才以及餐學院的專案中,引數校驗的工作都在前端完成,而後端介面只處理業務邏輯,但是這種方式不太合理,繞過頁面直接進行http請求,會有系統異常以及髒資料的風險,所以推薦使用Bean Validation 基於JSR 303 - Bean Validation 引數校驗框架在後端介面做引數校驗,格式化校驗,以及引數可選範圍的校驗,這樣既能規避大部分因引數缺失而產生的系統異常,也能在介面聯調階段,提高聯調效率,減少前後端同學在聯調時排查問題的時間
Hibernate Validator是 Bean Validation 的參考實現。Hibernate Validator 提供了 JSR 303 規範中所有內建 constraint 的實現,目前已升級到Bean Validation 2.0 / JSR - 380 ,除此之外還有一些附加的 constraint。該Hibernate不是ORM的Hibernate
舉例Bean Validation 中的 constraint (約束,限制),Bean Validation 的註解在javax.validation.constraints下
約束 | 限制 |
---|---|
@Null | 被註釋的元素必須為 null |
@NotNull | 被註釋的元素必須不為 null |
@AssertTrue | 被註釋的元素必須為 true |
@AssertFalse | 被註釋的元素必須為 false |
@Min(value) | 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@Max(value) | 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@DecimalMin(value) | 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@DecimalMax(value) | 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@Size(max, min) | 被註釋的元素的大小必須在指定的範圍內 |
@Digits (integer, fraction) | 被註釋的元素必須是一個數字,其值必須在可接受的範圍內 |
@Past | 被註釋的元素必須是一個過去的日期 |
@Future | 被註釋的元素必須是一個將來的日期 |
@Pattern(value) | 被註釋的元素必須符合指定的正則表示式 |
Hibernate Validator 附加的 constraint / Hibernate Validator是JSR - 303 的最好實現,目前規範已升級到 JSR
約束 | 限制 |
---|---|
被註釋的元素必須是電子郵箱地址 | |
@Length | 被註釋的字串的大小必須在指定的範圍內 |
@NotEmpty | 被註釋的字串的必須非空 |
@Range | 被註釋的元素必須在合適的範圍內 |
使用方法
Bean Validation 是JDK 1.6 +後內建的,包名為javax.validation.constraints
Hibernate Validator 則需要引入jar包,包名為org.hibernate.validator.constraints
POM.xml
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency> 複製程式碼
實體類
import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Past; import java.util.Date; public class ValidationDemo { private String id; @Length(min = 2, max = 6, message = "使用者名稱長度要求在{min}-{max}之間") @NotNull(message = "使用者名稱不可為空") private String userName; @Email(message = "郵箱格式錯誤") private String email; @Past(message = "出生日期錯誤") private Date birthDay; @Min(value = 18, message = "年齡錯誤") @Max(value = 80, message = "年齡錯誤") private Integer age; @Range(min = 0, max = 1, message = "性別選擇錯誤") private Integer sex; } 複製程式碼
關於@Valid和Validated的比較,根據實際需求需求選擇
@Valid : 沒有分組功能,可以用在方法、建構函式、方法引數和成員屬性(field)上,如果一個待驗證的pojo類,其中還包含了待驗證的物件,需要在待驗證物件上註解@valid,才能驗證待驗證物件中的成員屬性
@Validated :提供分組功能,可以在入參驗證時,根據不同的分組採用不同的驗證機制,用在型別、方法和方法引數上。但不能用於成員屬性(field)。
Controller
-- @Valid 表示對該實體進行校驗 -- BindingResult 則儲存對引數的校驗結果 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Valid @RequestBody ValidationDemo demo, BindingResult result) { JsonResult jsonResult = new JsonResult(); if (result.hasErrors()) { result.getAllErrors().forEach(err -> { jsonResult.setCode(ApiConstants.JsonResult.FAIL); jsonResult.setMsg(err.getDefaultMessage()); }); } return jsonResult; } 複製程式碼
RequestBody
{ "age": 19, "birthDay": "2019-04-14T09:05:39.604Z", "email": "string", "id": "string", "sex": 0, "userName": "string" } 複製程式碼
Response
{ "code": 1, "msg": "郵箱格式錯誤", "total": 0, "totalpage": 0 } 複製程式碼
由此可見,引數的校驗已經生效,因為email不符合@Email的校驗規則,具體校驗規則可以檢視@Email的實現EmailValidator.java
userName 的錯誤message 裡面有{min} - {max} ?
RequestBody
{ "age": 19, "birthDay": "2019-04-14T09:05:39.604Z", "email": "string", "id": "string", "sex": 0, "userName": "" } 複製程式碼
Response
{ "code": 1, "msg": "使用者名稱長度要求在2-6之間", "total": 0, "totalpage": 0 } 複製程式碼
Hibernate Validator 通過EL表示式獲取到了在@length中定義的min以及max屬性的值
在上面的Controller中,需要在在介面引數中,增加一個BindingResult來接收校驗的結果,每一個BindingResult與@Valid是一一對應的,如果有多個@Valid,那麼需要對個BindResult來儲存校驗結果
進階使用,統一處理校驗結果並返回前端
在 ResponseEntityExceptionHandler (Line 162) 中,如果驗證出現異常的時候是丟擲了MethodArgumentNotValidException
MethodArgumentNotValidException 描述: Exception to be thrown when validation on an argument annotated with {@code @Valid} fails. 當使用@Valid註解的引數驗證失敗是丟擲異常 複製程式碼
所以在BaseController中對MethodArgumentNotValidException進行處理
Controller
-- 對介面進行簡化,通過異常捕獲的方式對校驗結果返回給前端 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Valid @RequestBody ValidationDemo demo) { return null; } 複製程式碼
BaseController
if (e instanceof MethodArgumentNotValidException) { res.setCode(ApiConstants.JsonResult.FAIL); res.setMsg(JSONArray.toJSONString(((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()))); } 複製程式碼
Response
{ "code": 1, "msg": "[\"年齡錯誤\",\"郵箱格式錯誤\"]", "total": 0, "totalpage": 0 } 複製程式碼
分組校驗
在實際使用中,有可能我們針對一個屬性,有多個校驗規則,這時候就要使用到分組校驗了
改造實體
public class ValidationDemo { private String id; @Length(min = 2, max = 6, message = "使用者名稱長度要求在{min}-{max}之間") @NotNull(message = "使用者名稱不可為空") private String userName; // 表示分組為Adult時使用該校驗規則 @Email(message = "郵箱格式錯誤") @NotBlank(message = "郵箱不可為空", groups = {ValidationDemo.Adult.class}) private String email; @Past(message = "出生日期錯誤") private Date birthDay; @Min(value = 18, message = "年齡錯誤") @Max(value = 80, message = "年齡錯誤") private Integer age; @Range(min = 0, max = 1, message = "性別選擇錯誤") private Integer sex; // 新增兩個分組 public interface Adult { } public interface Minor { } } 複製程式碼
測試一下
// 這裡將分組設定為Minor,目的是不校驗郵箱欄位 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Validated({ValidationDemo.Adult.class}) @RequestBody ValidationDemo demo) { return null; } RequestBody: { "age": 0, "birthDay": "2019-04-14T10:39:08.501Z", "email": "", "id": "string", "sex": 0, "userName": "string" } Response: { "code": 1, "msg": "[\"郵箱不可為空\"]", "total": 0, "totalpage": 0 } 複製程式碼
如果是介面使用Minor分組呢?
RequestBody: { "age": 0, "birthDay": "2019-04-14T10:39:08.501Z", "email": "", "id": "string", "sex": 0, "userName": "string" } Response: { "code": 0, "data": [ {} ], "extra": "string", "msg": "string", "result": {}, "total": 0, "totalpage": 0 } 複製程式碼
並沒有提示郵箱不可為空,由此可見,分組驗證已經生效
自定義校驗規則
例如新建一個自定義日期格式的校驗
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Constraint(validatedBy = {DateFormatByPatternValidator.class}) public @interface DateFormatByPattern { String pattern() default "yyyy-MM-dd HH:mm"; //預設錯誤訊息 String message() default "日期格式錯誤"; //分組 Class<?>[] groups() default {}; //負載 Class<? extends Payload>[] payload() default {}; } 複製程式碼
同時新建一個對應的校驗器
public class DateFormatByPatternValidator implements ConstraintValidator<DateFormatByPattern, String> { private DateFormatByPattern dateFormatByPattern; @Override public void initialize(DateFormatByPattern constraintAnnotation) { dateFormatByPattern = constraintAnnotation; } @Override public boolean isValid(String value, ConstraintValidatorContext context) { //假如引數為空的話,返回true,如果要對引數值進行非空校驗的話,通過@NotNull來校驗,這樣與日期格式校驗解耦 if (StringUtils.isNotBlank(value)) { String pattern = dateFormatByPattern.pattern(); SimpleDateFormat dateFormat = new SimpleDateFormat(pattern); try { dateFormat.parse(value); } catch (ParseException e) { return false; } } return true; } } 複製程式碼
改造實體
//使用自定義規則校驗前端引數 @DateFormatByPattern(pattern = "yyyy-MM-dd") //因為同時用到了分組校驗,所以在stringDate上新增@Valid,使校驗生效 @Valid private String stringDate; 複製程式碼
測試一下
RequestBody: { "age": 0, "birthDay": "2019-04-15T08:23:21.683Z", "email": "", "id": "string", "sex": 0, "stringDate": "string", "userName": "string" } Response: { "code": 1, "msg": "[\"日期格式錯誤\",\"郵箱不可為空\",\"年齡錯誤\"]", "total": 0, "totalpage": 0 } 複製程式碼