From 9edb96ae57eaa8e7c2bf88f3cbfb88960f5c36ba Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 6 Aug 2025 14:51:13 +0200 Subject: [PATCH] Introduce default ProxyConfig bean and exposed interfaces attribute Taken into account by all proxy processors, this enables consistent proxy type defaulting in Spring Boot as well as consistent opting out for specific bean definitions. Closes gh-35286 Closes gh-35293 --- .../aop/config/AopConfigUtils.java | 31 +++--- .../AbstractAdvisingBeanPostProcessor.java | 5 +- .../aop/framework/CglibAopProxy.java | 2 +- .../aop/framework/JdkDynamicAopProxy.java | 4 +- .../aop/framework/ProxyConfig.java | 49 +++++++--- .../autoproxy/AbstractAutoProxyCreator.java | 52 ++++------ ...BeanFactoryAwareAdvisingPostProcessor.java | 23 +++-- .../framework/autoproxy/AutoProxyUtils.java | 72 ++++++++++++++ .../resilience/RetryInterceptorTests.java | 95 +++++++++++++++++++ 9 files changed, 268 insertions(+), 65 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java index a7a9e033d1f..3247fa213d3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java @@ -23,6 +23,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -96,17 +98,22 @@ public abstract class AopConfigUtils { } public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); - } + defaultProxyConfig(registry).getPropertyValues().add("proxyTargetClass", Boolean.TRUE); } public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + defaultProxyConfig(registry).getPropertyValues().add("exposeProxy", Boolean.TRUE); + } + + private static BeanDefinition defaultProxyConfig(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME)) { + return registry.getBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME); } + RootBeanDefinition beanDefinition = new RootBeanDefinition(ProxyConfig.class); + beanDefinition.setSource(AopConfigUtils.class); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, beanDefinition); + return beanDefinition; } private static @Nullable BeanDefinition registerOrEscalateApcAsRequired( @@ -115,12 +122,12 @@ public abstract class AopConfigUtils { Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - if (!cls.getName().equals(apcDefinition.getBeanClassName())) { - int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(beanDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { - apcDefinition.setBeanClassName(cls.getName()); + beanDefinition.setBeanClassName(cls.getName()); } } return null; @@ -128,8 +135,8 @@ public abstract class AopConfigUtils { RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); beanDefinition.setSource(source); - beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; } 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 3b7b1705243..70f0c63122e 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 @@ -112,11 +112,13 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu if (isEligible(bean, beanName)) { ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass()) { + if (!proxyFactory.isProxyTargetClass() && !proxyFactory.hasUserSuppliedInterfaces()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); + proxyFactory.setFrozen(isFrozen()); + proxyFactory.setPreFiltered(true); // Use original ClassLoader if bean class not locally loaded in overriding class loader ClassLoader classLoader = getProxyClassLoader(); @@ -187,6 +189,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); + proxyFactory.setFrozen(false); proxyFactory.setTarget(bean); return proxyFactory; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index be021f99f58..8ed496c3dad 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -694,7 +694,7 @@ class CglibAopProxy implements AopProxy, Serializable { Object target = null; TargetSource targetSource = this.advised.getTargetSource(); try { - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index a2b105839d2..b6601801d8a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -183,7 +183,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa // There is only getDecoratedClass() declared -> dispatch to proxy config. return AopProxyUtils.ultimateTargetClass(this.advised); } - else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + else if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) { // Service invocations on ProxyConfig with the proxy config... return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); @@ -191,7 +191,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa Object retVal; - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java index 3c4ee97be34..ca21266ba0e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java @@ -18,6 +18,8 @@ package org.springframework.aop.framework; import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -34,15 +36,15 @@ public class ProxyConfig implements Serializable { private static final long serialVersionUID = -8409359707199703185L; - private boolean proxyTargetClass = false; + private @Nullable Boolean proxyTargetClass; - private boolean optimize = false; + private @Nullable Boolean optimize; - boolean opaque = false; + private @Nullable Boolean opaque; - boolean exposeProxy = false; + private @Nullable Boolean exposeProxy; - private boolean frozen = false; + private @Nullable Boolean frozen; /** @@ -65,7 +67,7 @@ public class ProxyConfig implements Serializable { * Return whether to proxy the target class directly as well as any interfaces. */ public boolean isProxyTargetClass() { - return this.proxyTargetClass; + return (this.proxyTargetClass != null && this.proxyTargetClass); } /** @@ -85,7 +87,7 @@ public class ProxyConfig implements Serializable { * Return whether proxies should perform aggressive optimizations. */ public boolean isOptimize() { - return this.optimize; + return (this.optimize != null && this.optimize); } /** @@ -103,7 +105,7 @@ public class ProxyConfig implements Serializable { * prevented from being cast to {@link Advised}. */ public boolean isOpaque() { - return this.opaque; + return (this.opaque != null && this.opaque); } /** @@ -124,7 +126,7 @@ public class ProxyConfig implements Serializable { * each invocation. */ public boolean isExposeProxy() { - return this.exposeProxy; + return (this.exposeProxy != null && this.exposeProxy); } /** @@ -141,7 +143,7 @@ public class ProxyConfig implements Serializable { * Return whether the config is frozen, and no advice changes can be made. */ public boolean isFrozen() { - return this.frozen; + return (this.frozen != null && this.frozen); } @@ -153,9 +155,34 @@ public class ProxyConfig implements Serializable { Assert.notNull(other, "Other ProxyConfig object must not be null"); this.proxyTargetClass = other.proxyTargetClass; this.optimize = other.optimize; + this.opaque = other.opaque; this.exposeProxy = other.exposeProxy; this.frozen = other.frozen; - this.opaque = other.opaque; + } + + /** + * Copy default settings from the other config object, + * for settings that have not been locally set. + * @param other object to copy configuration from + * @since 7.0 + */ + public void copyDefault(ProxyConfig other) { + Assert.notNull(other, "Other ProxyConfig object must not be null"); + if (this.proxyTargetClass == null) { + this.proxyTargetClass = other.proxyTargetClass; + } + if (this.optimize == null) { + this.optimize = other.optimize; + } + if (this.opaque == null) { + this.opaque = other.opaque; + } + if (this.exposeProxy == null) { + this.exposeProxy = other.exposeProxy; + } + if (this.frozen == null) { + this.frozen = other.frozen; + } } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index c8c2f56f2bd..8e621fd2139 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -117,12 +117,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport /** Default is global AdvisorAdapterRegistry. */ private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); - /** - * Indicates whether the proxy should be frozen. Overridden from super - * to prevent the configuration from becoming frozen too early. - */ - private boolean freezeProxy = false; - /** Default is no common interceptors. */ private String[] interceptorNames = new String[0]; @@ -141,22 +135,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private final Map advisedBeans = new ConcurrentHashMap<>(256); - /** - * Set whether the proxy should be frozen, preventing advice - * from being added to it once it is created. - *

Overridden from the superclass to prevent the proxy configuration - * from being frozen before the proxy is created. - */ - @Override - public void setFrozen(boolean frozen) { - this.freezeProxy = frozen; - } - - @Override - public boolean isFrozen() { - return this.freezeProxy; - } - /** * Specify the {@link AdvisorAdapterRegistry} to use. *

Default is the global {@link AdvisorAdapterRegistry}. @@ -206,6 +184,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } /** @@ -471,6 +450,24 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); + proxyFactory.setFrozen(false); + + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + Class[] ifcs = (this.beanFactory instanceof ConfigurableListableBeanFactory clbf ? + AutoProxyUtils.determineExposedInterfaces(clbf, beanName) : null); + if (ifcs != null) { + proxyFactory.setProxyTargetClass(false); + for (Class ifc : ifcs) { + proxyFactory.addInterface(ifc); + } + } + else if (!proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } if (proxyFactory.isProxyTargetClass()) { // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) @@ -481,22 +478,13 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport } } } - else { - // No proxyTargetClass flag enforced, let's apply our default checks... - if (shouldProxyTargetClass(beanClass, beanName)) { - proxyFactory.setProxyTargetClass(true); - } - else { - evaluateProxyInterfaces(beanClass, proxyFactory); - } - } Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); - proxyFactory.setFrozen(this.freezeProxy); + proxyFactory.setFrozen(isFrozen()); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java index c9d07a1fdf8..256bdd5c9d5 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java @@ -25,9 +25,9 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; /** - * Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware}, - * adds exposure of the original target class for each proxied bean - * ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), + * Extension of {@link AbstractAdvisingBeanPostProcessor} which implements + * {@link BeanFactoryAware}, adds exposure of the original target class for each + * proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), * and participates in an externally enforced target-class mode for any given bean * ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}). * This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}. @@ -47,6 +47,7 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory clbf ? clbf : null); + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } @Override @@ -56,9 +57,19 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst } ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null && - AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { - proxyFactory.setProxyTargetClass(true); + if (this.beanFactory != null) { + if (AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + Class[] ifcs = AutoProxyUtils.determineExposedInterfaces(this.beanFactory, beanName); + if (ifcs != null) { + proxyFactory.setProxyTargetClass(false); + for (Class ifc : ifcs) { + proxyFactory.addInterface(ifc); + } + } + } } return proxyFactory; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java index b73b9abd5bb..3522bfd8b66 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java @@ -18,6 +18,8 @@ package org.springframework.aop.framework.autoproxy; import org.jspecify.annotations.Nullable; +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -31,9 +33,37 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @since 2.0.3 * @see AbstractAutoProxyCreator + * @see AbstractBeanFactoryAwareAdvisingPostProcessor */ public abstract class AutoProxyUtils { + /** + * The bean name of the internally managed auto-proxy creator. + * @since 7.0 + */ + public static final String DEFAULT_PROXY_CONFIG_BEAN_NAME = + "org.springframework.aop.framework.autoproxy.defaultProxyConfig"; + + /** + * Bean definition attribute that may indicate the interfaces to be proxied + * (in case of it getting proxied in the first place). The value is either + * a single interface {@code Class} or an array of {@code Class}, with an + * empty array specifically signalling that all implemented interfaces need + * to be proxied. + * @since 7.0 + * @see #determineExposedInterfaces + */ + public static final String EXPOSED_INTERFACES_ATTRIBUTE = + Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "exposedInterfaces"); + + /** + * Attribute value for specifically signalling that all implemented interfaces + * need to be proxied (through an empty {@code Class} array). + * @since 7.0 + * @see #EXPOSED_INTERFACES_ATTRIBUTE + */ + public static final Object ALL_INTERFACES_ATTRIBUTE_VALUE = new Class[0]; + /** * Bean definition attribute that may indicate whether a given bean is supposed * to be proxied with its target class (in case of it getting proxied in the first @@ -57,6 +87,47 @@ public abstract class AutoProxyUtils { Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass"); + /** + * Apply default ProxyConfig settings to the given ProxyConfig instance, if necessary. + * @param proxyConfig the current ProxyConfig instance + * @param beanFactory the BeanFactory to take the default ProxyConfig from + * @since 7.0 + * @see #DEFAULT_PROXY_CONFIG_BEAN_NAME + * @see ProxyConfig#copyDefault + */ + static void applyDefaultProxyConfig(ProxyConfig proxyConfig, BeanFactory beanFactory) { + if (beanFactory.containsBean(DEFAULT_PROXY_CONFIG_BEAN_NAME)) { + ProxyConfig defaultProxyConfig = beanFactory.getBean(DEFAULT_PROXY_CONFIG_BEAN_NAME, ProxyConfig.class); + proxyConfig.copyDefault(defaultProxyConfig); + } + } + + /** + * Determine the specific interfaces for proxying the given bean, if any. + * Checks the {@link #EXPOSED_INTERFACES_ATTRIBUTE "exposedInterfaces" attribute} + * of the corresponding bean definition. + * @param beanFactory the containing ConfigurableListableBeanFactory + * @param beanName the name of the bean + * @return whether the given bean should be proxied with its target class + * @since 7.0 + * @see #EXPOSED_INTERFACES_ATTRIBUTE + */ + static Class @Nullable [] determineExposedInterfaces( + ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { + + if (beanName != null && beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition bd = beanFactory.getBeanDefinition(beanName); + Object interfaces = bd.getAttribute(EXPOSED_INTERFACES_ATTRIBUTE); + if (interfaces instanceof Class[] ifcs) { + return ifcs; + } + else if (interfaces instanceof Class ifc) { + return new Class[] {ifc}; + } + } + return null; + } + /** * Determine whether the given bean should be proxied with its target * class rather than its interfaces. Checks the @@ -65,6 +136,7 @@ public abstract class AutoProxyUtils { * @param beanFactory the containing ConfigurableListableBeanFactory * @param beanName the name of the bean * @return whether the given bean should be proxied with its target class + * @see #PRESERVE_TARGET_CLASS_ATTRIBUTE */ public static boolean shouldProxyTargetClass( ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { diff --git a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java index 21577a7edcb..ab0a4feea7e 100644 --- a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java @@ -26,7 +26,10 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.framework.ProxyConfig; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -76,6 +79,78 @@ class RetryInterceptorTests { assertThat(target.counter).isEqualTo(6); } + @Test + void withPostProcessorForMethodWithInterface() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class)); + RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); + assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); + assertThat(target.counter).isEqualTo(6); + } + + @Test + void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() { + ProxyConfig defaultProxyConfig = new ProxyConfig(); + defaultProxyConfig.setProxyTargetClass(true); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig); + bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class)); + RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); + assertThat(target.counter).isEqualTo(6); + } + + @Test + void withPostProcessorForMethodWithInterfaceAndPreserveTargetClass() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class); + bd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); + bf.registerBeanDefinition("bean", bd); + RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); + assertThat(target.counter).isEqualTo(6); + } + + @Test + void withPostProcessorForMethodWithInterfaceAndExposeInterfaces() { + ProxyConfig defaultProxyConfig = new ProxyConfig(); + defaultProxyConfig.setProxyTargetClass(true); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig); + RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class); + bd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, AutoProxyUtils.ALL_INTERFACES_ATTRIBUTE_VALUE); + bf.registerBeanDefinition("bean", bd); + RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); + assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); + assertThat(target.counter).isEqualTo(6); + } + @Test void withPostProcessorForClass() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -160,6 +235,26 @@ class RetryInterceptorTests { } + static class AnnotatedMethodBeanWithInterface implements AnnotatedInterface { + + int counter = 0; + + @Retryable(maxAttempts = 5, delay = 10) + @Override + public void retryOperation() throws IOException { + counter++; + throw new IOException(Integer.toString(counter)); + } + } + + + interface AnnotatedInterface { + + @Retryable(maxAttempts = 5, delay = 10) + void retryOperation() throws IOException; + } + + @Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40, includes = IOException.class, excludes = AccessDeniedException.class, predicate = CustomPredicate.class)