From 082784e023fb82633c0b3b8d2d8f42257b360a96 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 2 Jun 2023 10:00:46 +0100 Subject: [PATCH] Extract delegate from MethodValidationInterceptor See gh-29825 --- .../MethodValidationDelegate.java | 161 ++++++++++++++++++ .../MethodValidationInterceptor.java | 85 +++------ 2 files changed, 182 insertions(+), 64 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationDelegate.java diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationDelegate.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationDelegate.java new file mode 100644 index 0000000000..5509d23eab --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationDelegate.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import java.lang.reflect.Method; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.executable.ExecutableValidator; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.validation.annotation.Validated; + +/** + * Helper class to apply method-level validation on annotated methods via + * {@link jakarta.validation.Valid}. + * + *

Used by {@link MethodValidationInterceptor}. + * + * @author Rossen Stoyanchev + * @since 6.1.0 + */ +public class MethodValidationDelegate { + + private final Supplier validator; + + + /** + * Create an instance using a default JSR-303 validator underneath. + */ + public MethodValidationDelegate() { + this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + } + + /** + * Create an instance using the given JSR-303 ValidatorFactory. + * @param validatorFactory the JSR-303 ValidatorFactory to use + */ + public MethodValidationDelegate(ValidatorFactory validatorFactory) { + this.validator = SingletonSupplier.of(validatorFactory::getValidator); + } + + /** + * Create an instance using the given JSR-303 Validator. + * @param validator the JSR-303 Validator to use + */ + public MethodValidationDelegate(Validator validator) { + this.validator = () -> validator; + } + + /** + * Create an instance for the supplied (potentially lazily initialized) Validator. + * @param validator a Supplier for the Validator to use + */ + public MethodValidationDelegate(Supplier validator) { + this.validator = validator; + } + + + /** + * Use this method determine the validation groups to pass into + * {@link #validateMethodArguments(Object, Method, Object[], Class[])} and + * {@link #validateMethodReturnValue(Object, Method, Object, Class[])}. + *

Default are the validation groups as specified in the {@link Validated} + * annotation on the method, or on the containing target class of the method, + * or for an AOP proxy without a target (with all behavior in advisors), also + * check on proxied interfaces. + * @param target the target Object + * @param method the target method + * @return the applicable validation groups as a {@code Class} array + */ + public Class[] determineValidationGroups(Object target, Method method) { + Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class); + if (validatedAnn == null) { + if (AopUtils.isAopProxy(target)) { + for (Class type : AopProxyUtils.proxiedUserInterfaces(target)) { + validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class); + if (validatedAnn != null) { + break; + } + } + } + else { + validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class); + } + } + return (validatedAnn != null ? validatedAnn.value() : new Class[0]); + } + + /** + * Validate the given method arguments and raise {@link ConstraintViolation} + * in case of any errors. + * @param target the target Object + * @param method the target method + * @param arguments candidate arguments for a method invocation + * @param groups groups for validation determined via + * {@link #determineValidationGroups(Object, Method)} + */ + public void validateMethodArguments(Object target, Method method, Object[] arguments, Class[] groups) { + ExecutableValidator execVal = this.validator.get().forExecutables(); + Set> result; + try { + result = execVal.validateParameters(target, method, arguments, groups); + } + catch (IllegalArgumentException ex) { + // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 + // Let's try to find the bridged method on the implementation class... + Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass()); + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod); + result = execVal.validateParameters(target, bridgedMethod, arguments, groups); + } + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + } + + /** + * Validate the given return value and raise {@link ConstraintViolation} + * in case of any errors. + * @param target the target Object + * @param method the target method + * @param returnValue value returned from invoking the target method + * @param groups groups for validation determined via + * {@link #determineValidationGroups(Object, Method)} + */ + public void validateMethodReturnValue( + Object target, Method method, @Nullable Object returnValue, Class[] groups) { + + ExecutableValidator execVal = this.validator.get().forExecutables(); + Set> result = execVal.validateReturnValue(target, method, returnValue, groups); + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index 059abb5d0d..b6dc9fbbe8 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -17,38 +17,28 @@ package org.springframework.validation.beanvalidation; import java.lang.reflect.Method; -import java.util.Set; import java.util.function.Supplier; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; -import jakarta.validation.executable.ExecutableValidator; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.annotation.Validated; /** * An AOP Alliance {@link MethodInterceptor} implementation that delegates to a * JSR-303 provider for performing method-level validation on annotated methods. * - *

Applicable methods have JSR-303 constraint annotations on their parameters - * and/or on their return value (in the latter case specified at the method level, - * typically as inline annotation). + *

Applicable methods have {@link jakarta.validation.Constraint} annotations on + * their parameters and/or on their return value (in the latter case specified at + * the method level, typically as inline annotation). * *

E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)} * @@ -65,14 +55,14 @@ import org.springframework.validation.annotation.Validated; */ public class MethodValidationInterceptor implements MethodInterceptor { - private final Supplier validator; + private final MethodValidationDelegate delegate; /** * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. */ public MethodValidationInterceptor() { - this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + this.delegate = new MethodValidationDelegate(); } /** @@ -80,7 +70,7 @@ public class MethodValidationInterceptor implements MethodInterceptor { * @param validatorFactory the JSR-303 ValidatorFactory to use */ public MethodValidationInterceptor(ValidatorFactory validatorFactory) { - this.validator = SingletonSupplier.of(validatorFactory::getValidator); + this.delegate = new MethodValidationDelegate(validatorFactory); } /** @@ -88,7 +78,7 @@ public class MethodValidationInterceptor implements MethodInterceptor { * @param validator the JSR-303 Validator to use */ public MethodValidationInterceptor(Validator validator) { - this.validator = () -> validator; + this.delegate = new MethodValidationDelegate(validator); } /** @@ -98,7 +88,7 @@ public class MethodValidationInterceptor implements MethodInterceptor { * @since 6.0 */ public MethodValidationInterceptor(Supplier validator) { - this.validator = validator; + this.delegate = new MethodValidationDelegate(validator); } @@ -110,42 +100,25 @@ public class MethodValidationInterceptor implements MethodInterceptor { return invocation.proceed(); } + Object target = getTarget(invocation); + Method method = invocation.getMethod(); Class[] groups = determineValidationGroups(invocation); + this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups); - // Standard Bean Validation 1.1 API - ExecutableValidator execVal = this.validator.get().forExecutables(); - Method methodToValidate = invocation.getMethod(); - Set> result; + Object returnValue = invocation.proceed(); + this.delegate.validateMethodReturnValue(target, method, returnValue, groups); + return returnValue; + } + + private static Object getTarget(MethodInvocation invocation) { Object target = invocation.getThis(); if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { // Allow validation for AOP proxy without a target target = methodInvocation.getProxy(); } Assert.state(target != null, "Target must not be null"); - - try { - result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); - } - catch (IllegalArgumentException ex) { - // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 - // Let's try to find the bridged method on the implementation class... - 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(); - - result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); - if (!result.isEmpty()) { - throw new ConstraintViolationException(result); - } - - return returnValue; + return target; } private boolean isFactoryBeanMetadataMethod(Method method) { @@ -178,25 +151,9 @@ public class MethodValidationInterceptor implements MethodInterceptor { * @return the applicable validation groups as a Class array */ protected Class[] determineValidationGroups(MethodInvocation invocation) { - Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); - if (validatedAnn == null) { - Object target = invocation.getThis(); - if (target != null) { - validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class); - } - else if (invocation instanceof ProxyMethodInvocation methodInvocation) { - Object proxy = methodInvocation.getProxy(); - if (AopUtils.isAopProxy(proxy)) { - for (Class type : AopProxyUtils.proxiedUserInterfaces(proxy)) { - validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class); - if (validatedAnn != null) { - break; - } - } - } - } - } - return (validatedAnn != null ? validatedAnn.value() : new Class[0]); + Object target = getTarget(invocation); + Method method = invocation.getMethod(); + return this.delegate.determineValidationGroups(target, method); } }