Preface
The Spring Validation validation framework provides a very convenient parameter validation function. You only need @Validated
or @Valid
and some rule annotations to verify the parameters.
I read many SpringBoot parameter verification tutorials on the based on 160f7b869916bc "Single Parameter Verification" and "Entity Class Parameter Verification" . The same, even this is more misleading).
This classification is easy to make people feel confusing: the annotation @Validated
will be marked on the class for a while, and then marked before the parameter; the exception must be dealt with BindException
ConstraintViolationException
must be dealt with.
You may still remember it when you first read it, but it will be easy to remember it after a while, especially when the two methods are in the same class at the same time, you can't remember how to use it. In the end, you may simply add @Validated
annotations.
This article will classify from the perspective of verification mechanism, SpringBoot's parameter verification has two sets of mechanisms, which will be controlled by the two sets of mechanisms at the time of execution. In addition to controlling their respective parts, the two mechanisms are partially overlapped, and this part will involve issues such as priority. But as long as you know what the two mechanisms are, and understand
The Spring process will never be confused anymore.
Verification mechanism
Of these two verification mechanisms, the first is controlled by SpringMVC. This kind of verification can only be used in the "Controller" layer. It is necessary to mark @Valid
, @Validated
front of the object to be verified, 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" layer, etc. can all be verified with this parameter. Need to mark @Validated
Annotation, and then if you verify a single type of parameter, mark @NotEmpty
directly in front of the parameter; if you verify the object, mark the @Valid
annotation in front of the object (here only @Valid
can be used, and the others cannot take effect. 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, get a general understanding of the SpringMVC execution process:
- Receive all requests initiated by the front end through DispatcherServlet
- Obtain the corresponding HandlerMapping through configuration and map the request to the processor. That is, according to the analysis of url, http protocol, request parameters, etc., find the information of the corresponding Method of the corresponding Controller.
- Obtain the corresponding HandlerAdapter through configuration, which is used 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 return data obtained by the previous call.
Parameter calibrating function is done at step 3, the client requests typically by RequestMappingHandlerAdapter
series configuration information and packaging, the final call to ServletInvocableHandlerMethod.invokeHandlerMethod()
method.
HandlerMethod
This ServletInvocableHandlerMethod
inherits InvocableHandlerMethod
, and its role is to call HandlerMethod
.
HandlerMethod
is a very important class in SpringMVC. The place that everyone most often touches is the third entry HandlerInterceptor
Object handler
, although this entry is Object
Type, but usually will be forced to HandlerMethod
. It is used to encapsulate the "Controller". Almost all the information that may be used when invoking, such as methods, method parameters, method annotations, and belonging classes, will be processed in advance and placed in this class.
HandlerMethod
itself only encapsulates and stores data and does not provide specific usage methods, so InvocableHandlerMethod
appears, which is responsible for executing HandlerMethod
, and ServletInvocableHandlerMethod
adds processing of return values and response status codes on its basis.
Here are the comments of the source author on these two classes:
InvocableHandlerMethod
calls the code of HandlerMethod
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 of mapping 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 method in the method is this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
, which calls the implementation class of the HandlerMethodArgumentResolver
HandlerMethodArgumentResolver
HandlerMethodArgumentResolver
is also a very important component part of SpringMVC. It is a strategy interface used to parse method parameters into parameter values, which is what we often call a custom parameter parser. The interface has two methods:
supportsParameter
method user determines whether the MethodParameter is processed by this Resolver
resolveArgument
method is used to parse the parameters into the input object of the method.
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 by 060f7b86991b38)
RequestParamMethodArgumentResolver
( @RequestParam
annotated by 060f7b86991b4d, or Java basic data types that are not matched by other Resolver)
RequestHeaderMethodArgumentResolver
( @RequestHeaderMethodArgumentResolver
annotated by 060f7b86991b63)
ServletModelAttributeMethodProcessor
( @ModelAttribute
annotated by 060f7b86991b84, or custom objects that are not matched by other resolvers) and so on.
Let's take ServletModelAttributeMethodProcessor
as an example to see how its 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 step 4 of the code, call the validateIfApplicable
method to check the name. Take a 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)
method is used to determine whether this parameter object has a comment that meets the parameter verification conditions, and returns the corresponding grouping information (the grouping function of @Validated
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 "this kind of verification can only be used in the "Controller" layer, and it needs to be marked with @Valid
, @Validated
, or a custom name starting with "Valid"" in front of the object to be verified. Do the verification code.
If it is @Validated
it returns @Validated
, otherwise it returns empty data. If there is no comment that meets the conditions, it returns null.
After determining the verification conditions, then binder.validate(validationHints);
will call SmartValidator
process the grouping information, and finally call the org.hibernate.validator.internal.engine.ValidatorImpl.validateValue
method to do the actual verification logic.
in conclusion:
The verification of SpringMVC is in
HandlerMethodArgumentResolver
, and the corresponding verification rules are written in the code implemented by theValidationAnnotationUtils.determineValidationHints(ann)
. The judgment of whether to verify is determined by 060f7b86991df9.However, only the
ModelAttributeMethodProcessor
AbstractMessageConverterMethodArgumentResolver
the two abstract classes 060f7b86991e1b and 060f7b86991e1d have written verification logic. The implementation classes are:ServletModelAttributeMethodProcessor (
@ModelAttribute
annotated by 060f7b86991e3d, or custom objects that are not matched by other Resolver)HttpEntityMethodProcessor (
HttpEntity
orRequestEntity
object)RequestPartMethodArgumentResolver (
@RequestPart
annotated byMultipartFile
or 060f7b86991e7b class)RequestResponseBodyMethodProcessor (
@RequestBody
annotated by 060f7b86991e95)
@RequestParam
annotated parameter or the Resolver of a single parameter that is often used in development does not implement the verification logic, but this part can also be verified in use, because this part of the verification is passed to the AOP mechanism for verification rule processing.
Detailed AOP verification mechanism
In the "Detailed Explanation of SpringMVC Verification Mechanism" section mentioned above, in the DispatcherServlet process, there will be InvocableHandlerMethod
calling HandlerMethod
code, here is a review:
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 getMethodArgumentValues
method is analyzed above, and the parameters in the request will be obtained and verified to be assembled into the parameters required by the Method. In this section, look at what the doInvoke(args)
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
obtains HandlerMethod
, and then calls the business code in the Controller we wrote through the Java native reflection function.
MethodValidationInterceptor
Since what is obtained here is a Bean object managed by Spring, it must have been "proxy". To be proxied, there must be a point of contact. Then look at @Validated
annotation has been called. Found that a MethodValidationInterceptor
was called,
At first glance, this name is related to the verification function and is an interceptor. Take a look at the annotations of this class.
The comment is very straightforward. The first sentence says that this is MethodInterceptor
, which provides method-level verification functions.
MethodValidationInterceptor
is regarded as the Advice part of the AOP mechanism, which is registered to Spring's AOP management MethodValidationPostProcessor
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
initializes the Bean, Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
creates a AnnotationMatchingPointcut
Validated
, which will make the class with 060f7b86992061 annotated as an AOP proxy.
So as long as it is a bean managed by Spring, the AOP mechanism can be used for parameter verification, and the class or interface of the method to be verified must have Validated
annotations. .
Now look at the code logic in MethodValidationInterceptor
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]);
}
}
Here invoke
proxy method mainly does several steps:
- Call the
determineValidationGroups
method to get the Group information in Validated. First look for theValidated
annotation on the method to obtain the grouping information, if not, use the grouping information annotated withValidated
- Get the validator class, usually
ValidatorImpl
- Call the verification method
ExecutableValidator.validateParameters
verify the input parameters, if it throwsIllegalArgumentException
Exception, try to obtain its generic information and verify again. If the parameter verification fails,ConstraintViolationException
exception will be thrown - Call the verification method
ExecutableValidator.validateReturnValue
verify the return value. If the parameter verification fails,ConstraintViolationException
exception will be thrown
in conclusion:
Spring MVC will call the corresponding business code of the Controller through reflection, and the called class is the class proxied by Spring AOP, which will use the AOP mechanism.
The verification function isMethodValidationInterceptor
class, theExecutableValidator.validateParameters
parameters, and theExecutableValidator.validateReturnValue
method is called to verify the return value.
Summary and comparison of SpringMVC and AOP verification mechanism
- SpringMVC only takes effect if there are
@Valid
,@Validated
before the method enters the parameter object, or the custom name starts with "Valid"; AOP needs to mark@Validated
, Then mark the verification rule comment before entering the method (eg:@NotBlank
), or mark@Valid
before the verification object. - SpringMVC does parameter verification in the
HandlerMethodArgumentResolver
implementation class, so it can only be validated at the Controller layer, and only part of theHandlerMethodArgumentResolver
implementation class has a verification function (such asRequestParamMethodArgumentResolver
does not); AOP is Spring's proxy mechanism, so only Spring proxy Bean
You can do the verification. HandlerMethodArgumentResolver
parameters of custom objects, but cannot verify the return value (now 060f7b869922e5 provided by Spring does not do this function, you can implement it by implementing the Resolver yourself); AOP can verify basic data types, and you can verify Check the return value.BindException
exception when the verification failsMethodArgumentNotValidException
has also become aBindException
of 060f7b8699230f in the Spring5.3 version); AOP
When the verification fails, theConstraintViolationException
exception will be thrown. (Tip: So you can determine which verification process to follow by throwing exceptions, which is convenient for locating problems).- When verifying at the Controller layer, the SpringMVC process will be followed first, and then the AOP verification process will be followed.
Original address: SptingBoot parameter verification mechanism in detail, using verification will no longer be confused
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。