Hello, I'm a passerby. For more high-quality articles, see my personal blog: http://itsoku.com
In project development, we often encounter various parameter verification, especially the verification of form parameters. When there are not many parameters, we can manually verify them in the controller, but once we encounter a post interface with many parameters that need to be verified, it will be exhausting to verify one by one.
In fact, the Spring framework provides us with an API for object verification, which can help us save the trouble of manually verifying interface parameters one by one.
This article will systematically study the use of Spring Validation with you, and understand the principles.
Easy to use
The Java API
specification (JSR303) defines the standard validation-api
for Bean validation, but does not provide an implementation. hibernate validation
is the implementation of this specification, and adds validation annotations such as @Email
, @Length
, etc.
Spring Validation
is a secondary encapsulation of hibernate validation
, which is used to support spring mvc parameter automatic verification. Next, we take the spring-boot
project as an example to introduce the use of Spring Validation
.
Introducing dependencies
If the spring-boot
version is less than 2.3.x, spring-boot-starter-web
will automatically import the hibernate-validator
dependency. If the spring-boot
version is greater than 2.3.x, you need to manually introduce dependencies:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency>
For web services, in order to prevent illegal parameters from affecting the business, parameter verification must be done at the Controller
layer! In most cases, request parameters are divided into the following two forms:
-
POST
,PUT
requests, userequestBody
to pass parameters; -
GET
request, userequestParam/PathVariable
to pass parameters.
In fact, whether it is requestBody parameter verification or method-level verification, it is ultimately to call Hibernate Validator
to perform verification, and Spring Validation
is just a layer of encapsulation. Let’s briefly introduce the parameter verification of requestBody
and requestParam/PathVariable
below!
requestBody parameter verification
POST
, PUT
requests generally use requestBody
to pass parameters, in this case, the backend uses DTO
objects take over. Just add the @Validated
annotation to the DTO
object to realize automatic parameter validation. For example, there is an interface for saving User
, which requires the length of userName
to be 2-10, and the length of account
and password
It is 6-20. If the verification fails, a MethodArgumentNotValidException
exception will be thrown, and Spring
will convert it to a 400 (Bad Request) request by default.
DTO means Data Transfer Object (Data Transfer Object), which is used for interactive transmission between server and client. The Bean object used to receive request parameters can be represented in the spring-web
project.
Declaring constraint annotations on DTO fields
@Data public class UserDTO { private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @NotNull @Length(min = 6, max = 20) private String account; @NotNull @Length(min = 6, max = 20) private String password; }
Declaring validation annotations on method parameters
@PostMapping("/save") public Result saveUser(@RequestBody @Validated UserDTO userDTO) { // Business logic processing will only be executed if the verification is passed return Result.ok(); }
In this case, both @Valid
and @Validated
can be used.
requestParam/PathVariable parameter verification
GET
requests generally use requestParam/PathVariable
to pass parameters. If there are many parameters (such as more than 6), it is recommended to use the DTO
object to receive.
Otherwise, it is recommended to flatten the parameters one by one into the method input parameters. In this case, you must mark the @Validated
annotation on the Controller
class, and declare constraint annotations (such as @Min
, etc.) . If the validation fails, a ConstraintViolationException
will be thrown.
The code example is as follows:
@RequestMapping("/api/user") @RestController @Validated public class UserController { // path variable @GetMapping("{userId}") public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) { // Business logic processing will only be executed if the verification is passed UserDTO userDTO = new UserDTO(); userDTO.setUserId(userId); userDTO.setAccount("11111111111111111"); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } // query parameters @GetMapping("getByAccount") public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) { // Business logic processing will only be executed if the verification is passed UserDTO userDTO = new UserDTO(); userDTO.setUserId(100000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } }
Unified exception handling
As mentioned earlier, if the verification fails, a MethodArgumentNotValidException
or ConstraintViolationException
will be thrown. In actual project development, unified exception handling is usually used to return a more friendly prompt.
For example, our system requires that no matter what exception is sent, the status code of http
must return 200
, and the business code is used to distinguish the abnormal situation of the system.
@RestControllerAdvice public class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException. class}) @ResponseStatus(HttpStatus. OK) @ResponseBody public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex. getBindingResult(); StringBuilder sb = new StringBuilder("Validation failed:"); for (FieldError fieldError : bindingResult. getFieldErrors()) { sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); return Result.fail(BusinessCode. Parameter verification failed, msg); } @ExceptionHandler({ConstraintViolationException. class}) @ResponseStatus(HttpStatus. OK) @ResponseBody public Result handleConstraintViolationException(ConstraintViolationException ex) { return Result.fail(BusinessCode. Parameter verification failed, ex.getMessage()); } }
Advanced usage
Group verification
In an actual project, multiple methods may need to use the same DTO
class to receive parameters, and the validation rules of different methods may be different. At this time, simply adding constraint annotations to the fields of the DTO class cannot solve this problem. Therefore, spring-validation
supports the function of group verification, which is specially used to solve such problems.
Still the above example, such as When saving User, UserId
can be empty, but when updating User
, UserId
The value of code> must be **>=10000000000000000L**; the verification rules of other fields are the same in both cases. The code example of using group verification at this time is as follows:
The applicable grouping information groups are declared on the constraint annotation
@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save. class, Update. class}) @Length(min = 2, max = 10, groups = {Save. class, Update. class}) private String userName; @NotNull(groups = {Save. class, Update. class}) @Length(min = 6, max = 20, groups = {Save. class, Update. class}) private String account; @NotNull(groups = {Save. class, Update. class}) @Length(min = 6, max = 20, groups = {Save. class, Update. class}) private String password; /** * Verify grouping when saving */ public interface Save { } /** * Verify group when updating */ public interface Update { } }
Specify the verification group on the @Validated annotation
@PostMapping("/save") public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) { // Business logic processing will only be executed if the verification is passed return Result.ok(); } @PostMapping("/update") public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { // Business logic processing will only be executed if the verification is passed return Result.ok(); }
Nested validation
In the previous example, the fields in the DTO
class are all basic data types and String
types. But in actual scenarios, it is possible that a certain field is also an object. In this case, nested validation can be used first.
For example, when User
information is saved above, it also contains Jo
b information. It should be noted that at this time, the corresponding field of the DTO
class must be marked with the @Valid
annotation.
@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save. class, Update. class}) @Length(min = 2, max = 10, groups = {Save. class, Update. class}) private String userName; @NotNull(groups = {Save. class, Update. class}) @Length(min = 6, max = 20, groups = {Save. class, Update. class}) private String account; @NotNull(groups = {Save. class, Update. class}) @Length(min = 6, max = 20, groups = {Save. class, Update. class}) private String password; @NotNull(groups = {Save. class, Update. class}) @Valid private Job job; @Data public static class Job { @Min(value = 1, groups = Update. class) private Long jobId; @NotNull(groups = {Save. class, Update. class}) @Length(min = 2, max = 10, groups = {Save. class, Update. class}) private String jobName; @NotNull(groups = {Save. class, Update. class}) @Length(min = 2, max = 10, groups = {Save. class, Update. class}) private String position; } /** * Verify grouping when saving */ public interface Save { } /** * Verify group when updating */ public interface Update { } }
Nested validations can be used in conjunction with grouped validations. In addition, the nested collection verification will verify each item in the collection, for example, the List
field will verify each list
in this list
code>Job objects are verified
Collection verification
If the request body directly passes the json array to the background, and you want to perform parameter verification on each item in the array. At this point, if we directly use list
or set
under java.util.Collection
to receive data, the parameter verification will not take effect! We can use a custom list
collection to receive parameters:
Wrap the List
type and declare the @Valid
annotation
public class ValidationList<E> implements List<E> { @Delegate // @Delegate is a lombok annotation @Valid // Be sure to add the @Valid annotation public List<E> list = new ArrayList<>(); // Be sure to remember to override the toString method @Override public String toString() { return list.toString(); } }
The @Delegate
annotation is limited by the lombok
version, and 1.18.6
and later versions can support it. If the verification fails, a NotReadablePropertyException
will be thrown, which can also be handled with a unified exception.
For example, if we need to save multiple User
objects at one time, the method of the Controller
layer can be written as follows:
@PostMapping("/saveList") public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) { // Business logic processing will only be executed if the verification is passed return Result.ok(); }
Custom validation
Business requirements are always more complex than these simple checks provided by the framework, and we can customize checks to meet our needs.
Customizing spring validation
is very simple. Suppose we customize the encrypted id (composed of numbers or a-f letters, 32-256 length) verification, which is mainly divided into two steps:
Custom constraint annotation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {EncryptIdValidator. class}) public @interface EncryptId { // default error message String message() default "encrypted id format error"; // group Class<?>[] groups() default {}; // load Class<? extends Payload>[] payload() default {}; }
Implement the ConstraintValidator interface to write a constraint validator
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> { private static final Pattern PATTERN = Pattern.compile("^[a-f\d]{32,256}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { // check if it is not null if (value != null) { Matcher matcher = PATTERN. matcher(value); return matcher. find(); } return true; } }
In this way, we can use @EncryptId
for parameter verification!
Programmatic validation
The above examples are all based on annotations to achieve automatic verification. In some cases, we may want to call verification programmatically. At this time, you can inject the javax.validation.Validator
object, and then call its api.
@Autowired private javax.validation.Validator globalValidator; // programmatic validation @PostMapping("/saveWithCodingValidate") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class); // If the verification is passed, validate is empty; otherwise, validate contains items that have not passed the verification if (validate. isEmpty()) { // Business logic processing will only be executed if the verification is passed } else { for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) { // Validation failed, do other logic System.out.println(userDTOConstraintViolation); } } return Result.ok(); }
Fail Fast
Spring Validation
will verify all fields by default before throwing an exception. You can enable the Fali Fast mode through some simple configurations, and return immediately once the verification fails.
@Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // fail-fast mode .failFast(true) .buildValidatorFactory(); return validatorFactory. getValidator(); }
Validator implementation principle
Principle of requestBody parameter verification
In spring-mvc
, RequestResponseBodyMethodProcessor
is used to parse the parameters of @RequestBody
and process the @ResponseBody
annotation method of the return value. Obviously, the logic for performing parameter verification must be in the method resolveArgument()
for parsing parameters:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter. nestedIfOptional(); / / Encapsulate the request data into the DTO object 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) { // perform data validation validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() & amp; & amp; 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); } }
It can be seen that resolveArgument()
calls validateIfApplicable()
for parameter verification.
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // Get parameter annotations, such as @RequestBody, @Valid, @Validated Annotation[] annotations = parameter. getParameterAnnotations(); for (Annotation ann : annotations) { // Try to get the @Validated annotation first Validated validatedAnn = AnnotationUtils. getAnnotation(ann, Validated. class); //If @Validated is directly marked, then the validation is enabled directly. //If not, then judge whether there is a comment starting with Valid before the parameter. 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}); //Perform validation binder.validate(validationHints); break; } } }
It can be seen that resolveArgument()
calls validateIfApplicable()
for parameter verification.
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // Get parameter annotations, such as @RequestBody, @Valid, @Validated Annotation[] annotations = parameter. getParameterAnnotations(); for (Annotation ann : annotations) { // Try to get the @Validated annotation first Validated validatedAnn = AnnotationUtils. getAnnotation(ann, Validated. class); //If @Validated is directly marked, then the validation is enabled directly. //If not, then judge whether there is a comment starting with Valid before the parameter. 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}); //Perform validation binder.validate(validationHints); break; } } }
Seeing this, you should be able to understand why the two annotations @Validated
and @Valid
can be mixed in this scenario. Let’s continue to look at the implementation of WebDataBinder.validate()
.
@Override public void validate(Object target, Errors errors, Object... validationHints) { if (this. targetValidator != null) { processConstraintViolations( //Call Hibernate Validator here to perform real validation this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } }
Finally, it was found that the bottom layer finally called Hibernate Validator for real validation processing.
Principle of parameter verification at the method level
The above-mentioned method of flattening parameters into method parameters one by one, and then declaring constraint annotations in front of each parameter is method-level parameter verification.
In fact, this method can be used in any Spring Bean
method, such as Controller/Service
and so on. The underlying implementation principle is AOP. Specifically, AOP
aspects are dynamically registered through MethodValidationPostProcessor
, and then MethodValidationInterceptor
is used to weave and enhance point-cutting methods.
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { @Override public void afterPropertiesSet() { //Create aspects for all `@Validated` marked beans Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); //Create Advisor for enhancement this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } //Create Advice, which is essentially a method interceptor protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
Then look at MethodValidationInterceptor:
public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { // No need to enhance the method, skip directly if (isFactoryBeanMetadataMethod(invocation. getMethod())) { return invocation. proceed(); } //Get group information Class<?>[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation. getMethod(); Set<ConstraintViolation<Object>> result; try { //The method enters the parameter verification, and finally entrusts it to Hibernate Validator to verify result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //An exception is thrown directly if (!result. isEmpty()) { throw new ConstraintViolationException(result); } //actual method call Object returnValue = invocation. proceed(); //Verify the return value, and finally entrust it to Hibernate Validator to verify result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); //An exception is thrown directly if (!result. isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } }
In fact, whether it is requestBody
parameter verification or method-level verification, it is ultimately to call Hibernate Validator
to perform verification, and Spring Validation
is just Made a layer of encapsulation.
Source: https://juejin.cn/post/7080419992386142215
↓↓↓ Click to read the original text and go directly to my personal blog. Are you watching?