Skip to content

Commit 082784e

Browse files
committed
Extract delegate from MethodValidationInterceptor
See gh-29825
1 parent 155a37d commit 082784e

File tree

2 files changed

+182
-64
lines changed

2 files changed

+182
-64
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.validation.beanvalidation;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.Set;
21+
import java.util.function.Supplier;
22+
23+
import jakarta.validation.ConstraintViolation;
24+
import jakarta.validation.ConstraintViolationException;
25+
import jakarta.validation.Validation;
26+
import jakarta.validation.Validator;
27+
import jakarta.validation.ValidatorFactory;
28+
import jakarta.validation.executable.ExecutableValidator;
29+
30+
import org.springframework.aop.framework.AopProxyUtils;
31+
import org.springframework.aop.support.AopUtils;
32+
import org.springframework.core.BridgeMethodResolver;
33+
import org.springframework.core.annotation.AnnotationUtils;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.util.ClassUtils;
36+
import org.springframework.util.function.SingletonSupplier;
37+
import org.springframework.validation.annotation.Validated;
38+
39+
/**
40+
* Helper class to apply method-level validation on annotated methods via
41+
* {@link jakarta.validation.Valid}.
42+
*
43+
* <p>Used by {@link MethodValidationInterceptor}.
44+
*
45+
* @author Rossen Stoyanchev
46+
* @since 6.1.0
47+
*/
48+
public class MethodValidationDelegate {
49+
50+
private final Supplier<Validator> validator;
51+
52+
53+
/**
54+
* Create an instance using a default JSR-303 validator underneath.
55+
*/
56+
public MethodValidationDelegate() {
57+
this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator());
58+
}
59+
60+
/**
61+
* Create an instance using the given JSR-303 ValidatorFactory.
62+
* @param validatorFactory the JSR-303 ValidatorFactory to use
63+
*/
64+
public MethodValidationDelegate(ValidatorFactory validatorFactory) {
65+
this.validator = SingletonSupplier.of(validatorFactory::getValidator);
66+
}
67+
68+
/**
69+
* Create an instance using the given JSR-303 Validator.
70+
* @param validator the JSR-303 Validator to use
71+
*/
72+
public MethodValidationDelegate(Validator validator) {
73+
this.validator = () -> validator;
74+
}
75+
76+
/**
77+
* Create an instance for the supplied (potentially lazily initialized) Validator.
78+
* @param validator a Supplier for the Validator to use
79+
*/
80+
public MethodValidationDelegate(Supplier<Validator> validator) {
81+
this.validator = validator;
82+
}
83+
84+
85+
/**
86+
* Use this method determine the validation groups to pass into
87+
* {@link #validateMethodArguments(Object, Method, Object[], Class[])} and
88+
* {@link #validateMethodReturnValue(Object, Method, Object, Class[])}.
89+
* <p>Default are the validation groups as specified in the {@link Validated}
90+
* annotation on the method, or on the containing target class of the method,
91+
* or for an AOP proxy without a target (with all behavior in advisors), also
92+
* check on proxied interfaces.
93+
* @param target the target Object
94+
* @param method the target method
95+
* @return the applicable validation groups as a {@code Class} array
96+
*/
97+
public Class<?>[] determineValidationGroups(Object target, Method method) {
98+
Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class);
99+
if (validatedAnn == null) {
100+
if (AopUtils.isAopProxy(target)) {
101+
for (Class<?> type : AopProxyUtils.proxiedUserInterfaces(target)) {
102+
validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class);
103+
if (validatedAnn != null) {
104+
break;
105+
}
106+
}
107+
}
108+
else {
109+
validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
110+
}
111+
}
112+
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
113+
}
114+
115+
/**
116+
* Validate the given method arguments and raise {@link ConstraintViolation}
117+
* in case of any errors.
118+
* @param target the target Object
119+
* @param method the target method
120+
* @param arguments candidate arguments for a method invocation
121+
* @param groups groups for validation determined via
122+
* {@link #determineValidationGroups(Object, Method)}
123+
*/
124+
public void validateMethodArguments(Object target, Method method, Object[] arguments, Class<?>[] groups) {
125+
ExecutableValidator execVal = this.validator.get().forExecutables();
126+
Set<ConstraintViolation<Object>> result;
127+
try {
128+
result = execVal.validateParameters(target, method, arguments, groups);
129+
}
130+
catch (IllegalArgumentException ex) {
131+
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
132+
// Let's try to find the bridged method on the implementation class...
133+
Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass());
134+
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod);
135+
result = execVal.validateParameters(target, bridgedMethod, arguments, groups);
136+
}
137+
if (!result.isEmpty()) {
138+
throw new ConstraintViolationException(result);
139+
}
140+
}
141+
142+
/**
143+
* Validate the given return value and raise {@link ConstraintViolation}
144+
* in case of any errors.
145+
* @param target the target Object
146+
* @param method the target method
147+
* @param returnValue value returned from invoking the target method
148+
* @param groups groups for validation determined via
149+
* {@link #determineValidationGroups(Object, Method)}
150+
*/
151+
public void validateMethodReturnValue(
152+
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
153+
154+
ExecutableValidator execVal = this.validator.get().forExecutables();
155+
Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups);
156+
if (!result.isEmpty()) {
157+
throw new ConstraintViolationException(result);
158+
}
159+
}
160+
161+
}

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java

Lines changed: 21 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,28 @@
1717
package org.springframework.validation.beanvalidation;
1818

1919
import java.lang.reflect.Method;
20-
import java.util.Set;
2120
import java.util.function.Supplier;
2221

23-
import jakarta.validation.ConstraintViolation;
24-
import jakarta.validation.ConstraintViolationException;
25-
import jakarta.validation.Validation;
2622
import jakarta.validation.Validator;
2723
import jakarta.validation.ValidatorFactory;
28-
import jakarta.validation.executable.ExecutableValidator;
2924
import org.aopalliance.intercept.MethodInterceptor;
3025
import org.aopalliance.intercept.MethodInvocation;
3126

3227
import org.springframework.aop.ProxyMethodInvocation;
33-
import org.springframework.aop.framework.AopProxyUtils;
34-
import org.springframework.aop.support.AopUtils;
3528
import org.springframework.beans.factory.FactoryBean;
3629
import org.springframework.beans.factory.SmartFactoryBean;
37-
import org.springframework.core.BridgeMethodResolver;
38-
import org.springframework.core.annotation.AnnotationUtils;
3930
import org.springframework.lang.Nullable;
4031
import org.springframework.util.Assert;
4132
import org.springframework.util.ClassUtils;
42-
import org.springframework.util.function.SingletonSupplier;
4333
import org.springframework.validation.annotation.Validated;
4434

4535
/**
4636
* An AOP Alliance {@link MethodInterceptor} implementation that delegates to a
4737
* JSR-303 provider for performing method-level validation on annotated methods.
4838
*
49-
* <p>Applicable methods have JSR-303 constraint annotations on their parameters
50-
* and/or on their return value (in the latter case specified at the method level,
51-
* typically as inline annotation).
39+
* <p>Applicable methods have {@link jakarta.validation.Constraint} annotations on
40+
* their parameters and/or on their return value (in the latter case specified at
41+
* the method level, typically as inline annotation).
5242
*
5343
* <p>E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
5444
*
@@ -65,30 +55,30 @@
6555
*/
6656
public class MethodValidationInterceptor implements MethodInterceptor {
6757

68-
private final Supplier<Validator> validator;
58+
private final MethodValidationDelegate delegate;
6959

7060

7161
/**
7262
* Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
7363
*/
7464
public MethodValidationInterceptor() {
75-
this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator());
65+
this.delegate = new MethodValidationDelegate();
7666
}
7767

7868
/**
7969
* Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
8070
* @param validatorFactory the JSR-303 ValidatorFactory to use
8171
*/
8272
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
83-
this.validator = SingletonSupplier.of(validatorFactory::getValidator);
73+
this.delegate = new MethodValidationDelegate(validatorFactory);
8474
}
8575

8676
/**
8777
* Create a new MethodValidationInterceptor using the given JSR-303 Validator.
8878
* @param validator the JSR-303 Validator to use
8979
*/
9080
public MethodValidationInterceptor(Validator validator) {
91-
this.validator = () -> validator;
81+
this.delegate = new MethodValidationDelegate(validator);
9282
}
9383

9484
/**
@@ -98,7 +88,7 @@ public MethodValidationInterceptor(Validator validator) {
9888
* @since 6.0
9989
*/
10090
public MethodValidationInterceptor(Supplier<Validator> validator) {
101-
this.validator = validator;
91+
this.delegate = new MethodValidationDelegate(validator);
10292
}
10393

10494

@@ -110,42 +100,25 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
110100
return invocation.proceed();
111101
}
112102

103+
Object target = getTarget(invocation);
104+
Method method = invocation.getMethod();
113105
Class<?>[] groups = determineValidationGroups(invocation);
106+
this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups);
114107

115-
// Standard Bean Validation 1.1 API
116-
ExecutableValidator execVal = this.validator.get().forExecutables();
117-
Method methodToValidate = invocation.getMethod();
118-
Set<ConstraintViolation<Object>> result;
108+
Object returnValue = invocation.proceed();
109+
110+
this.delegate.validateMethodReturnValue(target, method, returnValue, groups);
111+
return returnValue;
112+
}
119113

114+
private static Object getTarget(MethodInvocation invocation) {
120115
Object target = invocation.getThis();
121116
if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) {
122117
// Allow validation for AOP proxy without a target
123118
target = methodInvocation.getProxy();
124119
}
125120
Assert.state(target != null, "Target must not be null");
126-
127-
try {
128-
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
129-
}
130-
catch (IllegalArgumentException ex) {
131-
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
132-
// Let's try to find the bridged method on the implementation class...
133-
methodToValidate = BridgeMethodResolver.findBridgedMethod(
134-
ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
135-
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
136-
}
137-
if (!result.isEmpty()) {
138-
throw new ConstraintViolationException(result);
139-
}
140-
141-
Object returnValue = invocation.proceed();
142-
143-
result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
144-
if (!result.isEmpty()) {
145-
throw new ConstraintViolationException(result);
146-
}
147-
148-
return returnValue;
121+
return target;
149122
}
150123

151124
private boolean isFactoryBeanMetadataMethod(Method method) {
@@ -178,25 +151,9 @@ else if (FactoryBean.class.isAssignableFrom(clazz)) {
178151
* @return the applicable validation groups as a Class array
179152
*/
180153
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
181-
Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
182-
if (validatedAnn == null) {
183-
Object target = invocation.getThis();
184-
if (target != null) {
185-
validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
186-
}
187-
else if (invocation instanceof ProxyMethodInvocation methodInvocation) {
188-
Object proxy = methodInvocation.getProxy();
189-
if (AopUtils.isAopProxy(proxy)) {
190-
for (Class<?> type : AopProxyUtils.proxiedUserInterfaces(proxy)) {
191-
validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class);
192-
if (validatedAnn != null) {
193-
break;
194-
}
195-
}
196-
}
197-
}
198-
}
199-
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
154+
Object target = getTarget(invocation);
155+
Method method = invocation.getMethod();
156+
return this.delegate.determineValidationGroups(target, method);
200157
}
201158

202159
}

0 commit comments

Comments
 (0)