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
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
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