foreword
The Spring Validation validation framework provides a very convenient parameter validation function, which only requires @Validated
or @Valid
and some rule annotations to validate parameters.
I have seen many SpringBoot parameter verification tutorials on the Internet to classify them from the perspectives of "single parameter verification" and "entity class parameter verification" (or "Get method" and "Post method", which are actually the same, even This is more likely to be misunderstood). This kind of classification can easily make people feel confused: @Validated
annotations are marked on the class at one time, and before the parameters at another time; exceptions need to be handled BindException
again and again ConstraintViolationException
. You may remember it when you first read it, but it is easy to get confused after a while, especially when the two methods are in the same class at the same time, you don’t remember how to use them, and you may just add @Validated
annotations to all of them in the end.
This article is classified from the perspective of the verification mechanism. There are two mechanisms for parameter verification in SpringBoot, which will be controlled by the two mechanisms at the same time during execution. In addition to controlling their respective parts, the two sets of mechanisms overlap, and this part will involve issues such as priority. But once you know what the two mechanisms are, and you understand the Spring process, you'll never get confused again.
Check mechanism
These two sets of verification mechanisms, the first one is controlled by SpringMVC. This kind of validation can only be used in the "Controller" layer, and needs to be marked before the object to be verified @Valid
, @Validated
, or a custom annotation whose name starts with 'Valid', such as:
@Slfj
@RestController
@RequestMapping
public class ValidController {
@GetMapping("get1")
public void get1(@Validated ValidParam param) {
log.info("param: {}", param);
}
}
@Data
public class ValidParam {
@NotEmpty
private String name;
@Min(1)
private int age;
}
The other is controlled by AOP. This kind of bean can take effect as long as it is a Spring-managed bean, so the "Controller", "Service", "Dao" layers, etc. can all be verified with this parameter. An annotation needs to be marked on the class to be verified @Validated
, and then if a single type of parameter @NotEmpty
is verified, a verification rule annotation such as an annotation is directly marked in front of the parameter; if an object is verified, an annotation is marked in front of the object @Valid
(only used here @Valid
, Others cannot take effect, the reasons will be explained later), such as:
@Slf4j
@Validated
@RestController
@RequestMapping
public class ValidController {
/**
* 校验对象
*/
@GetMapping("get2")
public void get2(@Valid ValidParam param) {
log.info("param: {}", param);
}
/**
* 校验参数
*/
@GetMapping("get3")
public void get3(@NotEmpty String name, @Max(1) int age) {
log.info("name: {}, age: {}", name, age);
}
}
@Data
public class ValidParam {
@NotEmpty
private String name;
@Min(1)
private int age;
}
Detailed explanation of SpringMVC verification mechanism
First, let's have a general understanding of the SpringMVC execution process:
- Receive all front-end requests through DispatcherServlet
- Obtain the corresponding HandlerMapping through configuration, and map the request to the handler. That is to find the corresponding Method information of the corresponding Controller according to the parsing url, http protocol, request parameters, etc.
- Obtain the corresponding HandlerAdapter through configuration for actual processing and calling HandlerMapping. That is, the HandlerAdapter actually calls the method of the Controller written by the user.
- Obtain the corresponding ViewResolver through configuration, and process the returned data obtained by the previous call.
The function of parameter verification is done in step 3. The client request generally calls the method through RequestMappingHandlerAdapter
a series of configuration information and encapsulation .ServletInvocableHandlerMethod.invokeHandlerMethod()
HandlerMethod
This ServletInvocableHandlerMethod
inheritance InvocableHandlerMethod
, the role is responsible for calling HandlerMethod
.
HandlerMethod
HandlerInterceptor
It is a very important class in SpringMVC. The most common place that everyone touches is the third input parameter in the interceptor Object handler
. Although this input parameter is of Object
type, it is usually converted into HandlerMethod
. It is used to encapsulate "Controller", and almost all the information that may be used when calling, such as methods, method parameters, annotations on methods, and the class to which they belong, will be processed in advance and put into this class.
HandlerMethod
It only encapsulates and stores data, and does not provide specific usage methods, so InvocableHandlerMethod
it appears. It is responsible for execution HandlerMethod
, and ServletInvocableHandlerMethod
the processing of return values and response status codes is added on top of it.
Here is a note from the source author for these two classes:
InvocableHandlerMethod
Called HandlerMethod
code:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
The first line getMethodArgumentValues()
is the method that maps request parameters to Java objects. Let's take a look at this method:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 1. 获取 Method 方法中的入参信息
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
// 2. 初始化参数名的查找方式或框架,如反射,AspectJ、Kotlin 等
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// 3. 如果 getMethodArgumentValues() 方法第三个传参提供了一个参数,则这里用这个参数。(正常请求不会有这个参数,SpringMVC 处理异常的时候内部自己生成的)
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 4. 用对应的 HandlerMethodArgumentResolver 转换参数
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
The most important thing in the method is this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
to call HandlerMethodArgumentResolver
the implementation class of the interface to process the parameters.
HandlerMethodArgumentResolver
HandlerMethodArgumentResolver
It is also a very important component part in SpringMVC, a strategy interface for parsing method parameters into parameter values, which we often call custom parameter parser. The interface has two methods:
supportsParameter
The method user determines whether the MethodParameter is handled by this Resolver
resolveArgument
The method is used to parse the parameters into the method's input object.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
SpringMVC itself provides a lot of HandlerMethodArgumentResolver
implementation classes, such as:
RequestResponseBodyMethodProcessor
( @RequestBody
annotated parameter)
RequestParamMethodArgumentResolver
( @RequestParam
Annotated parameters, or Java primitive data types that no other resolver matches)
RequestHeaderMethodArgumentResolver
( @RequestHeaderMethodArgumentResolver
annotated parameter)
ServletModelAttributeMethodProcessor
( @ModelAttribute
Annotated parameters, or custom objects that no other resolver matches) and so on.
Let's take ServletModelAttributeMethodProcessor
an example to see how it looks resolveArgument
like:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// ...
// 获取参数名称以及异常处理等,这里省略。..
if (bindingResult == null) { // bindingResult 为空表示没有异常
// 1. binderFactory 创建对应的 DataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
// 2. 绑定数据,即实际注入数据到入参对象里
bindRequestParameters(binder, webRequest);
}
// 3. 校验数据,即 SpringMVC 参数校验的入口
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
// 4. 检查是否有 BindException 数据校验异常
throw new BindException(binder.getBindingResult());
}
}
if (!parameter.getParameterType().isInstance(attribute)) {
// 如果入参对象为 Optional 类型,SpringMVC 会帮忙转一下
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// 添加绑定结果到 mavContainer 中
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
In the code, step 4 calls validateIfApplicable
the method to see that the name is verified, and look at the code:
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
for (Annotation ann : parameter.getParameterAnnotations()) {
// 判定是否要做校验,同时获取 Validated 的分组信息
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if (validationHints != null) {
// 调用校验
binder.validate(validationHints);
break;
}
}
}
ValidationAnnotationUtils.determineValidationHints(ann)
The method is used to determine whether the parameter object has an annotation that meets the parameter verification conditions, and returns the corresponding grouping information ( @Validated
the grouping function).
public static Object[] determineValidationHints(Annotation ann) {
Class<? extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
// @Valid 注解
if ("javax.validation.Valid".equals(annotationName)) {
return EMPTY_OBJECT_ARRAY;
}
// @Validated 注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null) {
Object hints = validatedAnn.value();
return convertValidationHints(hints);
}
// 用户自定义的以 "Valid" 开头的注解
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
return convertValidationHints(hints);
}
return null;
}
Here is the SpringMVC that said at the beginning that "this kind of verification can only be used in the "Controller" layer, and needs to be marked in front of the object to be verified @Valid
, @Validated
, or a custom annotation whose name starts with 'Valid'" to determine whether to do the verification. test code. If it is @Validated
, return @Validated
the grouped data in it, otherwise return empty data, if there is no qualified annotation, return null.
After the verification conditions are determined, the processing group information binder.validate(validationHints);
will be called , and finally the method will be called to do the actual verification logic.SmartValidator
org.hibernate.validator.internal.engine.ValidatorImpl.validateValue
in conclusion:
The verification of SpringMVC is in
HandlerMethodArgumentResolver
the implementation class, and the corresponding verification rules are written in the code implemented by the resolveArgument method, and the judgment of whether to verify is determined by theValidationAnnotationUtils.determineValidationHints(ann)
origin .However, only the resolveArgument methods
ModelAttributeMethodProcessor
ofAbstractMessageConverterMethodArgumentResolver
these two abstract classes have written verification logic, and the implementation classes are:ServletModelAttributeMethodProcessor (
@ModelAttribute
annotated parameter, or a custom object that no other resolver matches)HttpEntityMethodProcessor (
HttpEntity
orRequestEntity
object)RequestPartMethodArgumentResolver (
@RequestPart
annotated parameter orMultipartFile
class)RequestResponseBodyMethodProcessor (
@RequestBody
annotated object)The parameters of annotations that are often used in development
@RequestParam
or the resolver of a single parameter does not implement the verification logic, but this part can also be verified in use, because this part of the verification is handed over to the verification rules of the AOP mechanism. of .
Detailed Explanation of AOP Verification Mechanism
In the "Detailed Explanation of SpringMVC Verification Mechanism" section above, it is mentioned that in the process of DispatcherServlet, there will be code to InvocableHandlerMethod
call . Let HandlerMethod
's review it here:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
This getMethodArgumentValues
method is analyzed above, and it will get the parameters in the request and verify the parameters required to assemble into the Method. In this section, let's see what doInvoke(args)
the method does.
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
ReflectionUtils.makeAccessible(method);
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
return method.invoke(getBean(), args);
} catch (IllegalArgumentException ex) {
// ...
// 一堆异常的处理,这里省略
}
}
doInvoke
Get HandlerMethod
the Method and Bean objects in it, and then call the business code in the Controller we wrote through the java native reflection function.
MethodValidationInterceptor
Since what is obtained here is the Bean object managed by Spring, it must have been "proxyed". If you want to proxy, you must have a pointcut, so let's see @Validated
what class the annotation is called by. It is found that a named MethodValidationInterceptor
class is called. This name is related to the verification function at first glance, and it is an interceptor. Take a look at the annotation of this class.
The comment is written very directly. The first sentence says that this is MethodInterceptor
the implementation class of AOP, which provides method-level verification functions.
MethodValidationInterceptor
It is the Advice part of the AOP mechanism, which is MethodValidationPostProcessor
registered by the class to Spring's AOP management:
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
// ...
// 省略一部分 set 代码。..
@Override
public void afterPropertiesSet() {
// 切点判定是否由 Validated 注解
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
afterPropertiesSet
When initializing the bean, a pointcut class is Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
created , and the annotations on the class are used as AOP proxy.AnnotationMatchingPointcut
Validated
Therefore, as long as the beans managed by Spring can use the AOP mechanism for parameter verification, there must be Validated
annotations on the class or interface where the method to be verified is located. .
Now let's take a look at MethodValidationInterceptor
the code logic in it:
public class MethodValidationInterceptor implements MethodInterceptor {
// ...
// 省略构造方法和 set 代码。..
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 跳过 FactoryBean 类的一些关键方法不校验
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
// 1. 获取 Validated 里的 Group 分组信息
Class<?>[] groups = determineValidationGroups(invocation);
// 2. 获取校验器类
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
// 3. 调用校验方法校验入参
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
} catch (IllegalArgumentException ex) {
// 处理对象里的泛型信息
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
// 4. 调用校验方法校验返回值
result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
if (validatedAnn == null) {
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
}
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
}
}
The invoke
proxy method here mainly does several steps:
- Call
determineValidationGroups
the method to obtain the Group grouping information in Validated. First look for theValidated
annotation on the method to obtain the grouping information, if not, useValidated
the grouping information of the annotation on the class. - Get the validator class, usually
ValidatorImpl
- Call the verification method to
ExecutableValidator.validateParameters
verify the input parameters. If an exception is thrownIllegalArgumentException
, try to obtain its generic information and verify it again. If the parameter verification fails, an exception will beConstraintViolationException
thrown - Call the verification method to
ExecutableValidator.validateReturnValue
verify the return value. If the parameter verification fails, an exception will beConstraintViolationException
thrown
To sum up: SpringMVC will call the business code corresponding to the Controller through reflection. The called class is the class that is proxied by Spring AOP, and will follow the AOP mechanism. The verification function is called in the
MethodValidationInterceptor
class, the callingExecutableValidator.validateParameters
method verifies the input parameters, and the callingExecutableValidator.validateReturnValue
method verifies the return value
Summary and comparison of SpringMVC and AOP verification mechanisms
- SpringMVC only takes effect if there is
@Valid
, before the method input object@Validated
, or the custom name starts with 'Valid'; AOP needs to be marked on the class first@Validated
, and then the validation rule annotation (such as:@NotBlank
) before the method input parameter, or the validation mark before the test object@Valid
. - SpringMVC
HandlerMethodArgumentResolver
performs parameter verification in the implementation class, so it can only be verified at the Controller layer, and only someHandlerMethodArgumentResolver
implementation classes have verification function (ifRequestParamMethodArgumentResolver
not); AOP is Spring's proxy mechanism, so as long as Spring proxy Bean Do check. - At present, SpringMVC verification can only verify the input parameters of custom objects, but cannot verify the return value (now Spring
HandlerMethodArgumentResolver
does not provide this function, you can achieve it by implementing your own resolver); AOP can verify basic data types and can verify return value. - SpringMVC will throw
BindException
an exception when the verification fails (MethodArgumentNotValidException
alsoBindException
a subclass in Spring 5.3); AOP verification throwsConstraintViolationException
an exception when the verification fails. (Tip: So you can determine which verification process to go through the thrown exception, which is convenient for locating the problem). - When verifying at the Controller layer, the SpringMVC process will be followed first, and then the AOP verification process will be followed.
Original address: Detailed explanation of the SptingBoot parameter verification mechanism, the use of verification is no longer confusing