From 5178e9c28eda20859829937c1bc3d38b8b314726 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 10 Jul 2022 20:09:49 +0200 Subject: [PATCH] Simplify hint registration for Spring AOP proxies Prior to this commit, when users wished to register proxy hints for a Spring AOP JDK dynamic proxy, they were required to explicitly specify SpringProxy, Advised, and DecoratingProxy along with user interfaces. This commit simplifies hint registration for Spring AOP proxies by introducing two completeJdkProxyInterfaces() methods in AopProxyUtils, one that accepts strings and one that accepts classes that represent the user-specified interfaces implemented the user component to be proxied. The SpringProxy, Advised, and DecoratingProxy interfaces are appended to the user-specified interfaces and returned as the complete set of interfaces that the proxy will implement. Closes gh-28745 --- .../aop/framework/AopProxyUtils.java | 76 +++++++++++++++++++ .../aop/framework/AopProxyUtilsTests.java | 53 +++++++++++++ ...MethodValidationRuntimeHintsRegistrar.java | 7 +- .../springframework/aot/hint/ProxyHints.java | 6 ++ ...ansactionBeanRegistrationAotProcessor.java | 12 +-- 5 files changed, 141 insertions(+), 13 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index 1f1def622c6..d8971947f6c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.springframework.aop.SpringProxy; @@ -28,6 +29,8 @@ import org.springframework.aop.TargetClassAware; import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.aot.hint.ProxyHints; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.core.DecoratingProxy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -93,6 +96,79 @@ public abstract class AopProxyUtils { return result; } + /** + * Complete the set of interfaces that are typically required in a JDK dynamic + * proxy generated by Spring AOP. + *

Specifically, {@link SpringProxy}, {@link Advised}, and {@link DecoratingProxy} + * will be appended to the set of user-specified interfaces. + *

Any {@linkplain Class#isSealed() sealed} interface in the set of + * user-specified interfaces will be omitted from the results, since only + * non-sealed interfaces are eligible for JDK dynamic proxies. + *

This method can be useful when registering {@linkplain ProxyHints proxy + * hints} for Spring's AOT support, as demonstrated in the following example + * which uses this method via a {@code static} import. + *

+	 * RuntimeHints hints = ...
+	 * hints.proxies().registerJdkProxy(completeJdkProxyInterfaces(MyInterface.class));
+	 * 
+ * @param userInterfaces the set of user-specified interfaces implemented by + * the component to be proxied + * @return the complete set of interfaces that the proxy should implement + * @since 6.0 + * @see SpringProxy + * @see Advised + * @see DecoratingProxy + * @see RuntimeHints#proxies() + * @see ProxyHints#registerJdkProxy(Class...) + * @see #completeJdkProxyInterfaces(String...) + */ + public static Class[] completeJdkProxyInterfaces(Class... userInterfaces) { + List> completedInterfaces = new ArrayList<>(userInterfaces.length + 3); + for (Class ifc : userInterfaces) { + Assert.isTrue(ifc.isInterface(), () -> ifc.getName() + " must be an interface"); + if (!ifc.isSealed()) { + completedInterfaces.add(ifc); + } + } + completedInterfaces.add(SpringProxy.class); + completedInterfaces.add(Advised.class); + completedInterfaces.add(DecoratingProxy.class); + return completedInterfaces.toArray(Class[]::new); + } + + /** + * Complete the set of interfaces that are typically required in a JDK dynamic + * proxy generated by Spring AOP. + *

Specifically, {@link SpringProxy}, {@link Advised}, and {@link DecoratingProxy} + * will be appended to the set of user-specified interfaces. + *

This method can be useful when registering {@linkplain ProxyHints proxy + * hints} for Spring's AOT support, as demonstrated in the following example + * which uses this method via a {@code static} import. + *

+	 * RuntimeHints hints = ...
+	 * hints.proxies().registerJdkProxy(completeJdkProxyInterfaces("com.example.MyInterface"));
+	 * 
+ * @param userInterfaces the set of fully qualified names of user-specified + * interfaces implemented by the component to be proxied + * @return the complete set of fully qualified names of interfaces that the + * proxy should implement + * @since 6.0 + * @see SpringProxy + * @see Advised + * @see DecoratingProxy + * @see RuntimeHints#proxies() + * @see ProxyHints#registerJdkProxy(Class...) + * @see #completeJdkProxyInterfaces(Class...) + */ + public static String[] completeJdkProxyInterfaces(String... userInterfaces) { + List completedInterfaces = new ArrayList<>(userInterfaces.length + 3); + Collections.addAll(completedInterfaces, userInterfaces); + completedInterfaces.add(SpringProxy.class.getName()); + completedInterfaces.add(Advised.class.getName()); + completedInterfaces.add(DecoratingProxy.class.getName()); + return completedInterfaces.toArray(String[]::new); + } + /** * Determine the complete set of interfaces to proxy for the given AOP configuration. *

This will always add the {@link Advised} interface unless the AdvisedSupport's diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java index 5d185351944..8fd095c6530 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.aop.SpringProxy; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.DecoratingProxy; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -108,4 +109,56 @@ class AopProxyUtilsTests { assertThatIllegalArgumentException().isThrownBy(() -> AopProxyUtils.proxiedUserInterfaces(proxy)); } + @Test + void completeJdkProxyInterfacesFromClassThatIsNotAnInterface() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class)) + .withMessage(TestBean.class.getName() + " must be an interface"); + } + + @Test + void completeJdkProxyInterfacesFromSingleClass() { + Class[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + @Test + void completeJdkProxyInterfacesFromMultipleClasses() { + Class[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, Comparable.class); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class, Comparable.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + @Test + void completeJdkProxyInterfacesIgnoresSealedInterfaces() { + Class[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class, Comparable.class); + assertThat(jdkProxyInterfaces).containsExactly( + Comparable.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + @Test + void completeJdkProxyInterfacesFromSingleClassName() { + String[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class.getName()); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class.getName(), SpringProxy.class.getName(), Advised.class.getName(), + DecoratingProxy.class.getName()); + } + + @Test + void completeJdkProxyInterfacesFromMultipleClassNames() { + String[] jdkProxyInterfaces = + AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class.getName(), Comparable.class.getName()); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class.getName(), Comparable.class.getName(), SpringProxy.class.getName(), + Advised.class.getName(), DecoratingProxy.class.getName()); + } + + + sealed interface SealedInterface { + } + + static final class SealedType implements SealedInterface { + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationRuntimeHintsRegistrar.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationRuntimeHintsRegistrar.java index 93d3e28c15a..9e3717e4868 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationRuntimeHintsRegistrar.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationRuntimeHintsRegistrar.java @@ -18,11 +18,9 @@ package org.springframework.validation.beanvalidation; import jakarta.validation.Validator; -import org.springframework.aop.SpringProxy; -import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.core.DecoratingProxy; import org.springframework.lang.Nullable; /** @@ -36,6 +34,7 @@ public class MethodValidationRuntimeHintsRegistrar implements RuntimeHintsRegist @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - hints.proxies().registerJdkProxy(Validator.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + hints.proxies().registerJdkProxy(AopProxyUtils.completeJdkProxyInterfaces(Validator.class)); } + } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ProxyHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ProxyHints.java index 053b6034530..31779e0b0e9 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ProxyHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ProxyHints.java @@ -80,6 +80,9 @@ public class ProxyHints { /** * Register that a JDK proxy implementing the specified interfaces is * required. + *

When registering a JDK proxy for Spring AOP, consider using + * {@link org.springframework.aop.framework.AopProxyUtils#completeJdkProxyInterfaces(Class...) + * AopProxyUtils.completeJdkProxyInterfaces()} for convenience. * @param proxiedInterfaces the interfaces the proxy should implement * @return {@code this}, to facilitate method chaining */ @@ -91,6 +94,9 @@ public class ProxyHints { /** * Register that a JDK proxy implementing the specified interfaces is * required. + *

When registering a JDK proxy for Spring AOP, consider using + * {@link org.springframework.aop.framework.AopProxyUtils#completeJdkProxyInterfaces(String...) + * AopProxyUtils.completeJdkProxyInterfaces()} for convenience. * @param proxiedInterfaces the fully qualified class names of interfaces the * proxy should implement * @return {@code this}, to facilitate method chaining diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java index 617a57e51d4..2b2efcf5066 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java @@ -20,8 +20,7 @@ import java.lang.reflect.AnnotatedElement; import java.util.LinkedHashSet; import java.util.Set; -import org.springframework.aop.SpringProxy; -import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -29,7 +28,6 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.core.DecoratingProxy; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -81,19 +79,15 @@ public class TransactionBeanRegistrationAotProcessor implements BeanRegistration @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - LinkedHashSet> interfaces = new LinkedHashSet<>(); Class[] proxyInterfaces = ClassUtils.getAllInterfacesForClass(this.beanClass); if (proxyInterfaces.length == 0) { return; } for (Class proxyInterface : proxyInterfaces) { - interfaces.add(proxyInterface); runtimeHints.reflection().registerType(proxyInterface, builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); } - interfaces.add(SpringProxy.class); - interfaces.add(Advised.class); - interfaces.add(DecoratingProxy.class); - runtimeHints.proxies().registerJdkProxy(interfaces.toArray(Class[]::new)); + runtimeHints.proxies().registerJdkProxy(AopProxyUtils.completeJdkProxyInterfaces(proxyInterfaces)); } } + }