Support method validation for interface-only proxies
Closes gh-29782
This commit is contained in:
parent
7da6e93597
commit
6a6a35a0b9
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
|
@ -97,6 +97,11 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
|
|||
if (this.beforeExistingAdvisors) {
|
||||
advised.addAdvisor(0, this.advisor);
|
||||
}
|
||||
else if (advised.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE && advised.getAdvisorCount() > 0) {
|
||||
// No target, leave last advisor in place
|
||||
advised.addAdvisor(advised.getAdvisorCount() - 1, this.advisor);
|
||||
return bean;
|
||||
}
|
||||
else {
|
||||
advised.addAdvisor(this.advisor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
|
@ -29,6 +29,9 @@ 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;
|
||||
|
|
@ -115,6 +118,10 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
Set<ConstraintViolation<Object>> result;
|
||||
|
||||
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 {
|
||||
|
|
@ -165,7 +172,8 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
/**
|
||||
* Determine the validation groups to validate against for the given method invocation.
|
||||
* <p>Default are the validation groups as specified in the {@link Validated} annotation
|
||||
* on the containing target class of the method.
|
||||
* 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 invocation the current MethodInvocation
|
||||
* @return the applicable validation groups as a Class array
|
||||
*/
|
||||
|
|
@ -173,8 +181,20 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
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);
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
|
|
@ -18,12 +18,15 @@ package org.springframework.validation.beanvalidation;
|
|||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.groups.Default;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
|
|
@ -34,9 +37,13 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
|
||||
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -71,6 +78,18 @@ public class MethodValidationTests {
|
|||
ac.close();
|
||||
}
|
||||
|
||||
@Test // gh-29782
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testMethodValidationPostProcessorForInterfaceOnlyProxy() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(MethodValidationPostProcessor.class);
|
||||
context.registerBean(MyValidInterface.class, () ->
|
||||
ProxyFactory.getProxy(MyValidInterface.class, new MyValidClientInterfaceMethodInterceptor()));
|
||||
context.refresh();
|
||||
doTestProxyValidation(context.getBean(MyValidInterface.class));
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void doTestProxyValidation(MyValidInterface<String> proxy) {
|
||||
assertThat(proxy.myValidMethod("value", 5)).isNotNull();
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() ->
|
||||
|
|
@ -156,6 +175,7 @@ public class MethodValidationTests {
|
|||
}
|
||||
|
||||
|
||||
@MyStereotype
|
||||
public interface MyValidInterface<T> {
|
||||
|
||||
@NotNull Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2);
|
||||
|
|
@ -167,6 +187,26 @@ public class MethodValidationTests {
|
|||
}
|
||||
|
||||
|
||||
static class MyValidClientInterfaceMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private final MyValidBean myValidBean = new MyValidBean();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
Method method;
|
||||
try {
|
||||
method = ClassUtils.getMethod(MyValidBean.class, invocation.getMethod().getName(), null);
|
||||
}
|
||||
catch (IllegalStateException ex) {
|
||||
method = BridgeMethodResolver.findBridgedMethod(
|
||||
ClassUtils.getMostSpecificMethod(invocation.getMethod(), MyValidBean.class));
|
||||
}
|
||||
return ReflectionUtils.invokeMethod(method, this.myValidBean, invocation.getArguments());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface MyGroup {
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue