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 1f1def622c..d8971947f6 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 5d18535194..8fd095c653 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 93d3e28c15..9e3717e486 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 053b603453..31779e0b0e 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 617a57e51d..2b2efcf506 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)); } } + }