From 6a6a35a0b95ac946c66c102ff0a43f0a638b5421 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 27 Jan 2023 14:45:40 +0000 Subject: [PATCH] Support method validation for interface-only proxies Closes gh-29782 --- .../AbstractAdvisingBeanPostProcessor.java | 7 +++- .../MethodValidationInterceptor.java | 28 +++++++++++-- .../beanvalidation/MethodValidationTests.java | 42 ++++++++++++++++++- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java index 68c54557a11..afde719ad30 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java @@ -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); } 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 b95032d07fb..059abb5d0d3 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 @@ -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> 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. *

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]); } diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java index 974f431462a..62e74041b5b 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java @@ -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 proxy) { assertThat(proxy.myValidMethod("value", 5)).isNotNull(); assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> @@ -156,6 +175,7 @@ public class MethodValidationTests { } + @MyStereotype public interface MyValidInterface { @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 { }