您好,登錄后才能下訂單哦!
這篇文章主要介紹“Spring注解@Validated失效怎么解決”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Spring注解@Validated失效怎么解決”文章能幫助大家解決問題。
@Validated 注解的作用這里就不多做介紹了,具體用法在網上應該有不少。
在之前使用 MVC 架構編碼時,通常是將 @Validated 注解或者 @Valid 配置在 Controller 的方法中,如下代碼所示:
@PostMapping("common/set") public Response<?> setCommonSetting(@RequestBody @Validated SetCommonSettingReqVO reqVO) { //doSomeThings return Response.success(); }
所以在配置應用層校驗時,就想當然的按照類似的寫法:
public void addClueTrack(@Validated AddClueTrackCommand command) { //doSomeThings }
結果可想而知,@Validated 注解并不生效。
竟然不生效,那么就開始分析原因。
首先可以很容易想到,竟然能在方法執行前就攔截進行校驗,那么大概率是使用動態代理。就和 @Transactional 事務注解一樣,底層都是基于 AOP 實現動態代理。
接下來為了印證這個想法,就是需要深入看看 Spring 實現的。通過 IDE 可以很方便看到有哪些地方引用了 @Validated 注解:
其中一個類名一下就引起了我的注意 MethodValidationPostProcessor,熟悉 Spring 的小伙伴應該知道,Spring 中有很多 BeanPostProcessor 用于擴展 Bean,Aop 便是基于此實現動態代理的。點進去一看,果不其然:
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { private Class<? extends Annotation> validatedAnnotationType = Validated.class; @Nullable private Validator validator; //... @Override public void afterPropertiesSet() { //創建切點 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()); } } public class AnnotationMatchingPointcut implements Pointcut { private final ClassFilter classFilter; private final MethodMatcher methodMatcher; public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType, boolean checkInherited) { //切點只針對類級別 this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited); this.methodMatcher = MethodMatcher.TRUE; } //... }
MethodValidationPostProcessor 中創建了一個切點,過濾類上添加了 @Validated 的 Bean,只要滿足此條件,就會根據 MethodValidationInterceptor 生成對應的代理類。嗯,和 @Transactional 的實現原理差不多。
ok,看到這里我就在應用服務實現上添加了 @Validated 注解,那么此時注解生效了嗎?哈哈,進度條還沒過半呢????
理論上類上加上 @Validated 注解,應該會生成動態代理類的,竟然沒成功進行參數校驗,我能想到的原因有二:
1. MethodValidationPostProcessor 沒注入到 BeanFactory 中,所以沒生成對應的代理類 2. MethodValidationInterceptor 對還有其他需要滿足的條件,而目前還未滿足
這里先劇透一下,答案是 2 ????
竟然答案是2,那這里就先講一下 MethodValidationInterceptor,MethodValidationPostProcessor 是怎么注冊到容器的咱們后面再來講。
ExecutableValidatorpublic class MethodValidationInterceptor implements MethodInterceptor { private final Validator validator; @Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { // Standard Bean Validation 1.1 API ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; //獲取類本身的實例(非代理類),請記住這里,這里就是和 Controller 最大的區別 Object target = invocation.getThis(); Assert.state(target != null, "Target must not be null"); try { //執行參數校驗,校驗的是當前類,也就是說校驗的是 Bean 對應的類 result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { //doSomeThings } if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //執行方法 Object returnValue = invocation.proceed(); //校驗返回值 result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } }
接下來就要看看 ExecutableValidator.validateParameters 這個方法是如何實現的,為了方便閱讀,這里我只保留了部分核心代碼。根據包名我們大概也能猜到 ExecutableValidator.validateParameters 是 hibernate-validator 包提供的方法,而 @Validated 注解是由 Spring 提供的,所以不生效也就正常了。接下來我們繼續往下走,我這里只貼部分核心的代碼,中間的棧路徑可以根據以下這個路徑往下走:
/** * --> org.hibernate.validator.internal.engine.ValidatorImpl#validateParameters * --> org.hibernate.validator.internal.metadata.BeanMetaDataManager#getBeanMetaData * --> org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl#createBeanMetaData * --> org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl#getBeanConfigurationForHierarchy * --> org.hibernate.validator.internal.metadata.provider.MetaDataProvider#getBeanConfiguration * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#retrieveBeanConfiguration * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getFieldMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findPropertyMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findConstraints * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findCascadingMetaData * <-- ... * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getMethodMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getConstructorMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getClassLevelConstraints * <-- ... * --> org.hibernate.validator.internal.metadata.aggregated.BeanMetaData#hasConstraints * --> org.hibernate.validator.internal.engine.ValidatorImpl#validateParametersInContext * */ public class ValidatorImpl implements Validator, ExecutableValidator { @Override public final <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups) { Contracts.assertNotNull( beanType, MESSAGES.beanTypeCannotBeNull() ); sanityCheckPropertyPath( propertyName ); sanityCheckGroups( groups ); //獲取 bean 及其父類、超類的 BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( beanType ); //判斷該 bean 是否有約束 if ( !rootBeanMetaData.hasConstraints() ) { return Collections.emptySet(); } PathImpl propertyPath = PathImpl.createPathFromString( propertyName ); BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidateValue( beanType, rootBeanMetaData, propertyPath ); ValidationOrder validationOrder = determineGroupValidationOrder( groups ); //校驗參數 return validateValueInContext(validationContext, value, propertyPath, validationOrder); } //... }
當我調試到 rootBeanMetaData.hasConstraints() 時,判斷沒有約束,然后就直接返回了沒有進行參數校驗。我就想說看看是如何判斷 Bean 是否有約束的,于是就返回上層進入 beanMetaDataManager.getBeanMetaData 中看,結果發現里面的代碼有夠復雜的????
public class AnnotationMetaDataProvider implements MetaDataProvider { //獲取類上所有的約束條件 private <T> BeanConfiguration<T> retrieveBeanConfiguration(Class<T> beanClass) { //獲取字段上的約束條件 Set<ConstrainedElement> constrainedElements = getFieldMetaData( beanClass ); //獲取方法上的約束條件(包括參數、返回值) constrainedElements.addAll( getMethodMetaData( beanClass ) ); //獲取構造函數 constrainedElements.addAll( getConstructorMetaData( beanClass ) ); //獲取類上的約束條件 Set<MetaConstraint<?>> classLevelConstraints = getClassLevelConstraints( beanClass ); if ( !classLevelConstraints.isEmpty() ) { ConstrainedType classLevelMetaData = new ConstrainedType(ConfigurationSource.ANNOTATION, beanClass, classLevelConstraints); constrainedElements.add( classLevelMetaData ); } return new BeanConfiguration<>(ConfigurationSource.ANNOTATION, beanClass, constrainedElements, getDefaultGroupSequence( beanClass ), getDefaultGroupSequenceProvider( beanClass )); } //查找約束注解 protected <A extends Annotation> List<ConstraintDescriptorImpl<?>> findConstraintAnnotations(Constrainable constrainable, A annotation, ConstraintLocationKind type) { //如果包含 "jdk.internal" and "java" 下的注解,則直接不進行校驗 if ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) { return Collections.emptyList(); } List<Annotation> constraints = newArrayList(); Class<? extends Annotation> annotationType = annotation.annotationType(); //判斷是否有約束條件,也就我們經常配置的 @NotNull,@Min 這類注解 if ( constraintCreationContext.getConstraintHelper().isConstraintAnnotation( annotationType ) ) { constraints.add( annotation ); } //這個沒用過,暫時跳過 else if ( constraintCreationContext.getConstraintHelper().isMultiValueConstraint( annotationType ) ) { constraints.addAll( constraintCreationContext.getConstraintHelper().getConstraintsFromMultiValueConstraint( annotation ) ); } return constraints.stream() .map( c -> buildConstraintDescriptor( constrainable, c, type ) ) .collect( Collectors.toList() ); } //構建級聯元數據構造器,也就是我們常用的 @Valid,在 Bean 中如果我們要對對象屬性進行校驗, //需要在該屬性上添加 @Valid,此處便是如此 private CascadingMetaDataBuilder getCascadingMetaData(JavaBeanAnnotatedElement annotatedElement, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) { return CascadingMetaDataBuilder.annotatedObject( annotatedElement.getType(), annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement.getAnnotatedType() ) ); } }
順著上面的棧路徑一直往下走,最終發現最核心的幾個方法是 getFieldMetaData、getMethodMetaData、getConstructorMetaData、getClassLevelConstraints,這個幾方法都是用于獲取約束和級聯元數據。那么里面到底是怎么獲取約束元數據的呢,咱繼續往里鉆,可以看到最終調用了 findConstraintAnnotations 獲取約束元數據,也就是我們平時用到的 @NotNull,@Min 等注解,通過 getCascadingMetaData 獲取級聯元數據,也就是 @Valid 注解。看到這,是不是很容易就能想到,知道我加上 @Valid 就能成功校驗了呢?
于是我嘗試了一波,果然沒問題。嗯~ 長見識了????。由于時間有限,ValidatorImpl.validateParametersInContext() 方法我就沒有深入進去看了。感興趣的小伙伴可以自行去看看!!????
明白了在應用服務實現,準確的說應該是普通 Bean 中應該怎么配置之 @Validated 和 @Valid 使其生效之后,我就很好奇為啥 Controller 只需要單獨在方法上配置 @Validated 或者 @Valid 就能成功校驗呢?
還記得上面通過 IDE 查看應用 @Validated 注解的類時,我們發現了 MethodValidationPostProcessor,還有另外幾個類一看就很像 Controller 參數解析相關的類:
我在這幾個類上各打了一個斷點,最終進入的是 AbstractMessageConverterMethodArgumentResolver。
ok,那就看看他是怎么實現的,這里只貼了很參數校驗相關的方法:
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { //獲取分組信息 Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { //進行校驗 binder.validate(validationHints); break; } } } } public abstract class ValidationAnnotationUtils { @Nullable 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); } if (annotationType.getSimpleName().startsWith("Valid")) { Object hints = AnnotationUtils.getValue(ann); return convertValidationHints(hints); } return null; } } public class DataBinder implements PropertyEditorRegistry, TypeConverter { public void validate(Object... validationHints) { //此處是關鍵所在,這里獲取的是參數!!!和普通的 Bean 獲取到的卻是 Bean 本身 Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result for (Validator validator : getValidators()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { ((SmartValidator) validator).validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } } }
可以看到,對于 Controller 不論是直接在參數上加上 @Validated 或者 @Valid 注解,都會進入到校驗方法,而且校驗的就是參數!!!而 Bean 校驗的卻是 Bean 本身!!!
明白了 @Validated 的攔截實現的原理后,那么就只剩最后一個問題了,MethodValidationPostProcessor 和 AbstractMessageConverterMethodArgumentResolver 是怎么被注冊到 BeanFactory 的。
其實不用看源碼大概有也能猜到是 Spring Boot 自動裝配的。為了印證一下,我還是貼一下源碼:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") @Import(PrimaryDefaultValidatorPostProcessor.class) public class ValidationAutoConfiguration { //... @Bean @ConditionalOnMissingBean public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) { FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream()); boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true); processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); return processor; } }
另外就是 AbstractMessageConverterMethodArgumentResolver 的幾個實現類,均由 RequestMappingHandlerAdapter 實例化,而 RequestMappingHandlerAdapter 大家知道有 WebMvcAutoConfiguration 自動裝配,時間原因,這就不看了。
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } }
關于“Spring注解@Validated失效怎么解決”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。