在Web項目中經常需要驗證前台的參數,比如驗證param != null 或者驗證param 的長度、集合的大小等等。一種辦法就是手動驗證,那就是寫大量的if代碼塊,另一種就是使用現成的validation。
@Valid 注解位於包 javax.validation; @Validated 注解位於包org.springframework.validation.annotation, 是Spring 提供的注解。
@Validated是@Valid 的一次封裝,是Spring提供的校驗機制使用。@Valid不提供分組功能,而@Validated 提供分組的功能。
1. 引入相關依賴
<!-- validate 相關注解 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.1.Final</version> </dependency>
2. 使用
1. 不帶分組的使用:
接收前端參數的Bean:
package com.xm.ggn.test; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.util.List; @Data public class User2 implements Serializable { @NotNull(message = "username 不能為空") @Length(min = 2, max = 20, message = "username 長度必須在{min}到{max}之間") private String username; @NotNull(message = "age 不能為空") @Range(min = 18, max = 25, message = "年齡必須在{min}-{max}之間") private Integer age; /** * 愛好 */ @NotEmpty(message = "愛好不能為空") private List<String> hobbies; private String fullname; }
兩個測試Controller:
@PostMapping("/user/add2") public User2 addUser2(@RequestBody @Valid User2 user) { System.out.println(user); return user; } @PostMapping("/user/add3") public User2 addUser3(@RequestBody @Validated User2 user) { System.out.println(user); return user; }
全局異常處理器: (捕捉到上面接口參數驗證失敗拋出的異常,然后提取到錯誤信息返回給前端)
package com.xm.ggn.exception; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.val; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindingResult; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.xm.ggn.utils.JSONResultUtil; import com.xm.ggn.utils.constant.ErrorCodeDefine; import lombok.extern.slf4j.Slf4j; /** * 全局異常處理器 * * @author Administrator * */ @RestControllerAdvice @Slf4j public class BXExceptionHandler { @ExceptionHandler(value = Throwable.class) public JSONResultUtil<Object> errorHandler(HttpServletRequest reqest, HttpServletResponse response, Exception e) { log.error("MyExceptionHandler errorHandler", e); // token錯誤 /* * if (e instanceof InvalidAccessTokenException) { * InvalidAccessTokenException exception = (InvalidAccessTokenException) * e; return JSONResultUtil.errorWithMsg("u100004", * exception.getMessage()); } */ // SpringMVC映射的參數沒傳值,導致映射參數失敗 if (e instanceof HttpMessageNotReadableException) { return JSONResultUtil.error("必傳參數為空"); } // @Valid 參數驗證失敗錯誤信息解析 if (e instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e; BindingResult bindingResult = exception.getBindingResult(); int errorCount = bindingResult.getErrorCount(); if (errorCount > 0) { String defaultMessage = bindingResult.getAllErrors().get(0).getDefaultMessage(); return JSONResultUtil.error(defaultMessage); } String message = exception.getMessage(); return JSONResultUtil.error(message); } if (e instanceof HttpRequestMethodNotSupportedException) { return JSONResultUtil.error("u100001"); } // 代碼用ValidateUtils 工具類進行參數校驗拋出的異常 if (e instanceof BxIllegalArgumentException) { BxIllegalArgumentException exception = (BxIllegalArgumentException) e; return JSONResultUtil.errorWithMsg(exception.getErrorCode(), exception.getMessage()); } return JSONResultUtil.error(ErrorCodeDefine.SYSTEM_ERROR); } }
測試Curl:
qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz"}' http://localhost:8088//user/add2 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 94 0 76 100 18 6909 1636 --:--:-- --:--:-- --:--:-- 9400{"success":false,"data":null,"msg":"age 不能為空","errorCode":"u100000"} qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 90}' http://localhost:8088//user/add3 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 115 0 86 100 29 3909 1318 --:--:-- --:--:-- --:--:-- 5476{"success":false,"data":null,"msg":"年齡必須在18-25之間","errorCode":"u100000"} qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add3 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 173 0 119 100 54 11900 5400 --:--:-- --:--:-- --:--:-- 19222{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null},"msg":"成功","errorCode":"0"}
2 測試分組的使用-分組只能針對Spring 提供的注解Validated
帶分組的功能是說可以在@Validated 注解可以聲明驗證的指定的分組。
1. 修改接收對象的實體如下
package com.xm.ggn.test; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.util.List; @Data public class User2 implements Serializable { @NotNull(message = "username 不能為空") @Length(min = 2, max = 20, message = "username 長度必須在{min}到{max}之間") private String username; @NotNull(message = "age 不能為空") @Range(min = 18, max = 25, message = "年齡必須在{min}-{max}之間") private Integer age; /** * 愛好 */ @NotEmpty(message = "愛好不能為空") private List<String> hobbies; private String fullname; /** * 密碼,只在新增的時候進行驗證 */ @NotNull(message = "password 不能為空", groups = {AddUser.class}) @Length(min = 6, max = 20, message = "password 長度必須在{min}到{max}之間", groups = {AddUser.class}) private String password; /** * 內部累標記是新增操作 */ public static interface AddUser { } }
這里需要注意groups 聲明的class 必須是接口類型。
2. 修改Controller
@PostMapping("/user/add3") public User2 addUser3(@RequestBody @Validated User2 user) { System.out.println(user); return user; } @PostMapping("/user/add5") public User2 addUser5(@RequestBody @Validated(User2.AddUser.class) User2 user) { System.out.println(user); return user; }
3. 測試
qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add3 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 189 0 135 100 54 10384 4153 --:--:-- --:--:-- --:--:-- 15750{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null,"password":null},"msg":"成功","errorCode":"0"} qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 135 0 81 100 54 8100 5400 --:--:-- --:--:-- --:--:-- 13500{"success":false,"data":null,"msg":"password 不能為空","errorCode":"u100000"} qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"password": "111222"}' http://localhost:8088//user/add5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 156 0 134 100 22 8375 1375 --:--:-- --:--:-- --:--:-- 10400{"success":true,"data":{"username":null,"age":null,"hobbies":null,"fullname":null,"password":"111222"},"msg":"成功","errorCode":"0"}
可以看出這里如果@Validated 指定了class 會只驗證指定class 分組的信息,如果不指定會驗證所有不帶組號的規則。
補充:其實@Valid 相當於不帶屬性的@Validated, 默認也是驗證不帶 groups 屬性的驗證規則
比如測試如下:
(1) 接受參數的Bean
package com.xm.ggn.test; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.util.List; @Data public class User2 implements Serializable { @NotNull(message = "username 不能為空") @Length(min = 2, max = 20, message = "username 長度必須在{min}到{max}之間") private String username; @NotNull(message = "age 不能為空") @Range(min = 18, max = 25, message = "年齡必須在{min}-{max}之間") private Integer age; /** * 愛好 */ @NotEmpty(message = "愛好不能為空") private List<String> hobbies; private String fullname; /** * 密碼,只在新增的時候進行驗證 */ @NotNull(message = "password 不能為空", groups = {AddUser.class}) @Length(min = 6, max = 20, message = "password 長度必須在{min}到{max}之間", groups = {AddUser.class}) private String password; /** * 內部累標記是新增操作 */ public static interface AddUser { } }
(2) Controller
@PostMapping("/user/add2") public User2 addUser2(@RequestBody @Valid User2 user) { System.out.println(user); return user; }
(3) curl 測試
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add2 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 189 0 135 100 54 182 73 --:--:-- --:--:-- --:--:-- 255{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null,"password":null},"msg":"成功","errorCode":"0"}
可以看出password 屬性驗證規則帶有groups 屬性,沒有被驗證到。
2. @Valid、@Validated 參數驗證原理
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument 參數解析過程中進行驗證
@Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); }
validateIfApplicable(binder, parameter); 是進行參數校驗,並且將校驗結果收集到binder.getBindingResult() 中。
if 語句進行判斷如果有驗證不通過的,並且 isBindExceptionRequired 方法判斷是否需要拋出異常。其判斷邏輯如下:
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { int i = parameter.getParameterIndex(); Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes(); boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); return !hasBindingResult; }
如果當前驗證參數的下一個參數是Errors 的子類則不拋出異常,異常會封裝到 Errors 的子類中。 如果下一個參數不是Errors 的子類,則走上面拋出異常的代碼 throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());所以看到這里可以發現有兩種處理方式:
第一種是用 BindingResult bindingResult 接受錯誤結果;
@PostMapping("/user/add4") public User2 addUser4(@RequestBody @Validated User2 user, BindingResult bindingResult) { System.out.println(bindingResult); System.out.println(user); return user; }
第二種是 增加全局異常攔截器,攔截上面的異常,然后給前端返回對應的錯誤信息:
Controller:
@PostMapping("/user/add3") public User2 addUser3(@RequestBody @Validated User2 user) { System.out.println(user); return user; }
全局異常攔截器:
package com.xm.ggn.exception; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.val; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindingResult; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.xm.ggn.utils.JSONResultUtil; import com.xm.ggn.utils.constant.ErrorCodeDefine; import lombok.extern.slf4j.Slf4j; /** * 全局異常處理器 * * @author Administrator * */ @RestControllerAdvice @Slf4j public class BXExceptionHandler { @ExceptionHandler(value = Throwable.class) public JSONResultUtil<Object> errorHandler(HttpServletRequest reqest, HttpServletResponse response, Exception e) { log.error("MyExceptionHandler errorHandler", e); // token錯誤 /* * if (e instanceof InvalidAccessTokenException) { * InvalidAccessTokenException exception = (InvalidAccessTokenException) * e; return JSONResultUtil.errorWithMsg("u100004", * exception.getMessage()); } */ // SpringMVC映射的參數沒傳值,導致映射參數失敗 if (e instanceof HttpMessageNotReadableException) { return JSONResultUtil.error("必傳參數為空"); } // @Valid 參數驗證失敗錯誤信息解析 if (e instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e; BindingResult bindingResult = exception.getBindingResult(); int errorCount = bindingResult.getErrorCount(); if (errorCount > 0) { String defaultMessage = bindingResult.getAllErrors().get(0).getDefaultMessage(); return JSONResultUtil.error(defaultMessage); } String message = exception.getMessage(); return JSONResultUtil.error(message); } if (e instanceof HttpRequestMethodNotSupportedException) { return JSONResultUtil.error("u100001"); } // 代碼用ValidateUtils 工具類進行參數校驗拋出的異常 if (e instanceof BxIllegalArgumentException) { BxIllegalArgumentException exception = (BxIllegalArgumentException) e; return JSONResultUtil.errorWithMsg(exception.getErrorCode(), exception.getMessage()); } return JSONResultUtil.error(ErrorCodeDefine.SYSTEM_ERROR); } }
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable 方法進行驗證, 這個驗證過程會將驗證的結果收集到binder.getBindingResult()。所以核心的邏輯是在這個方法內部。這個方法里面首先拿注解Validated 或者 判斷注解是否是以Valid 開始。 這里也就確定了上面兩個注解 @Valid 和 @Validated 都會被驗證。
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); binder.validate(validationHints); break; } } }
這個方法里面首先拿注解Validated 或者 判斷注解是否是以Valid 開始。 這里也就確定了上面兩個注解 @Valid 和 @Validated 都會被驗證。
然后其核心邏輯會調用到: org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateConstraints(org.hibernate.validator.internal.engine.ValidationContext<T>, org.hibernate.validator.internal.engine.ValueContext<?,V>, java.util.Set<javax.validation.ConstraintViolation<T>>)
private <T, V> void validateConstraints(ValidationContext<T> validationContext, ValueContext<?, V> valueContext, Set<ConstraintViolation<T>> constraintViolations) { CompositionResult compositionResult = validateComposingConstraints( validationContext, valueContext, constraintViolations ); Set<ConstraintViolation<T>> localViolations; // After all children are validated the actual ConstraintValidator of the constraint itself is executed if ( mainConstraintNeedsEvaluation( validationContext, constraintViolations ) ) { if ( log.isTraceEnabled() ) { log.tracef( "Validating value %s against constraint defined by %s.", valueContext.getCurrentValidatedValue(), descriptor ); } // find the right constraint validator ConstraintValidator<A, V> validator = getInitializedConstraintValidator( validationContext, valueContext ); // create a constraint validator context ConstraintValidatorContextImpl constraintValidatorContext = new ConstraintValidatorContextImpl( validationContext.getParameterNames(), validationContext.getTimeProvider(), valueContext.getPropertyPath(), descriptor ); // validate localViolations = validateSingleConstraint( validationContext, valueContext, constraintValidatorContext, validator ); // We re-evaluate the boolean composition by taking into consideration also the violations // from the local constraintValidator if ( localViolations.isEmpty() ) { compositionResult.setAtLeastOneTrue( true ); } else { compositionResult.setAllTrue( false ); } } else { localViolations = Collections.emptySet(); } if ( !passesCompositionTypeRequirement( constraintViolations, compositionResult ) ) { prepareFinalConstraintViolations( validationContext, valueContext, constraintViolations, localViolations ); } }
下面這行代碼會獲取到合適的驗證器ConstraintValidator, 每個驗證注解都有對應的validator 與之對應。
ConstraintValidator<A, V> validator = getInitializedConstraintValidator( validationContext, valueContext ); 尋找合適的validater,比如對於 @NotNull 獲取到對應的Validator 是org.hibernate.validator.internal.constraintvalidators.bv.NotNullValidator
最后請求到達: org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#getInitializedValidator 方法內部從緩存中沒有獲取到相關的信息,然后調用下面方法進行獲取
org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#createAndInitializeValidator 創建和初始化相關的validator
然后將validator 放到緩存中
最后調用 javax.validation.ConstraintValidator#isValid 進行驗證。
org.hibernate.validator.internal.metadata.core.ConstraintHelper#ConstraintHelper 構造里面維護了驗證注解與驗證器的關系, 會在容器啟動過程中進行調用然后維護其關系
3. 自定義自己的Validator
模仿org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator 進行書寫
(1) 定義注解
package com.xm.ggn.test.contraint; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = NumberValidator.class) public @interface Number { String message() default "非法的數字"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int min() default 0; int max() default Integer.MAX_VALUE; }
(2) 編寫驗證器
package com.xm.ggn.test.contraint; import org.hibernate.validator.internal.util.logging.Log; import org.hibernate.validator.internal.util.logging.LoggerFactory; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class NumberValidator implements ConstraintValidator<Number, Integer> { private static final Log log = LoggerFactory.make(); private int min; private int max; @Override public void initialize(Number parameters) { min = parameters.min(); max = parameters.max(); validateParameters(); } @Override public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) { return true; } return value >= min && value <= max; } private void validateParameters() { if (min < 0) { throw log.getMinCannotBeNegativeException(); } if (max < 0) { throw log.getMaxCannotBeNegativeException(); } if (max < min) { throw log.getLengthCannotBeNegativeException(); } } }
(3) 測試
package com.xm.ggn.test; import com.xm.ggn.test.contraint.Number; import lombok.Data; import javax.validation.constraints.NotNull; import java.io.Serializable; @Data public class User3 implements Serializable { @NotNull(message = "age 不能為空") @Number(min = 18, max = 25, message = "年齡必須在{min}-{max}之間") private Integer age; }
測試Controller
@PostMapping("/user/add6") public User3 addUser6(@RequestBody @Validated User3 user) { System.out.println(user); return user; }
curl 測試:
qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master) $ curl -X POST --header 'Content-Type: application/json' -d '{"age": 2}' http://localhost:8088//user/add6 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 96 0 86 100 10 174 20 --:--:-- --:--:-- --:--:-- 194{"success":false,"data":null,"msg":"年齡必須在18-25之間","errorCode":"u100000"}
補充:嵌套檢查
有的時候驗證需要嵌套驗證,比如接參數的是一個集合,集合里面是具體的bean。這時候集合需要加@Valid注解
(1) controller
@PostMapping("/user/batchAdd") public void batchAdd(@RequestBody @Validated UserRequestVO requestVO) { }
(2) UserRequestVO
package com.xm.ggn.test; import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.List; @Data public class UserRequestVO { @NotNull @Valid private List<User> users; }
(3) User
package com.xm.ggn.test; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; import java.io.Serializable; @Data public class User implements Serializable { @NotNull(message = "username 不能為空") @Length(min = 2, max = 20, message = "username 長度必須在{min}到{max}之間") private String username; @NotNull(message = "age 不能為空") @Range(min = 18, max = 25, message = "年齡必須在{min}-{max}之間") private Integer age; private String fullname; private String createDate; }