From df86a9973db6e88c12771003c4a4c667ad5b47ad Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 6 Aug 2025 18:26:40 +0200 Subject: [PATCH] Introduce @Proxyable annotation for bean-specific proxy type Closes gh-35296 See gh-35293 --- .../autoproxy/AbstractAutoProxyCreator.java | 2 +- .../annotation/AnnotationConfigUtils.java | 15 ++++ .../example/scannable/FooServiceImpl.java | 3 +- .../example/scannable/OtherFooService.java | 46 ++++++++++ ...anningCandidateComponentProviderTests.java | 31 ++++--- .../EnableAspectJAutoProxyTests.java | 4 +- ...figurationClassAspectIntegrationTests.java | 89 ++++++++++++++++++- .../example/scannable/spring.components | 7 +- 8 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 spring-context/src/test/java/example/scannable/OtherFooService.java 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 8e621fd2139..ccdf9187722 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 @@ -464,7 +464,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport proxyFactory.addInterface(ifc); } } - else if (!proxyFactory.isProxyTargetClass()) { + if (ifcs != null ? ifcs.length == 0 : !proxyFactory.isProxyTargetClass()) { evaluateProxyInterfaces(beanClass, proxyFactory); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index bc83fecad7b..7c116824bd6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -22,6 +22,7 @@ import java.util.function.Predicate; import org.jspecify.annotations.Nullable; +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; @@ -258,6 +259,20 @@ public abstract class AnnotationConfigUtils { if (description != null) { abd.setDescription(description.getString("value")); } + + AnnotationAttributes proxyable = attributesFor(metadata, Proxyable.class); + if (proxyable != null) { + ProxyType mode = proxyable.getEnum("value"); + if (mode == ProxyType.TARGET_CLASS) { + abd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); + } + else { + Class[] ifcs = proxyable.getClassArray("interfaces"); + if (ifcs.length > 0 || mode == ProxyType.INTERFACES) { + abd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, ifcs); + } + } + } } static BeanDefinitionHolder applyScopedProxyMode( diff --git a/spring-context/src/test/java/example/scannable/FooServiceImpl.java b/spring-context/src/test/java/example/scannable/FooServiceImpl.java index 8e8c3b09d81..441d5282cb3 100644 --- a/spring-context/src/test/java/example/scannable/FooServiceImpl.java +++ b/spring-context/src/test/java/example/scannable/FooServiceImpl.java @@ -33,6 +33,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.MessageSource; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; @@ -43,7 +44,7 @@ import org.springframework.util.Assert; * @author Mark Fisher * @author Juergen Hoeller */ -@Service @Lazy @DependsOn("myNamedComponent") +@Service @Primary @Lazy @DependsOn("myNamedComponent") public abstract class FooServiceImpl implements FooService { // Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces diff --git a/spring-context/src/test/java/example/scannable/OtherFooService.java b/spring-context/src/test/java/example/scannable/OtherFooService.java new file mode 100644 index 00000000000..23fe99961b9 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/OtherFooService.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.scannable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.springframework.context.annotation.Proxyable; +import org.springframework.stereotype.Service; + +/** + * @author Juergen Hoeller + */ +@Service @Proxyable(interfaces = FooService.class) +public class OtherFooService implements FooService { + + @Override + public String foo(int id) { + return "" + id; + } + + @Override + public Future asyncFoo(int id) { + return CompletableFuture.completedFuture("" + id); + } + + @Override + public boolean isInitCalled() { + return false; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index 144834bd895..c6ef88e0a8a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -41,6 +41,7 @@ import example.scannable.JakartaNamedComponent; import example.scannable.MessageBean; import example.scannable.NamedComponent; import example.scannable.NamedStubDao; +import example.scannable.OtherFooService; import example.scannable.ScopedProxyTestBean; import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; @@ -85,13 +86,13 @@ class ClassPathScanningCandidateComponentProviderTests { private static final Set> springComponents = Set.of( DefaultNamedComponent.class, - NamedComponent.class, FooServiceImpl.class, - StubFooDao.class, + NamedComponent.class, NamedStubDao.class, + OtherFooService.class, ServiceInvocationCounter.class, - BarComponent.class - ); + StubFooDao.class, + BarComponent.class); @Test @@ -213,7 +214,8 @@ class ClassPathScanningCandidateComponentProviderTests { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); // Interfaces/Abstract class are filtered out automatically. - assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class); + assertBeanTypes(candidates, + AutowiredQualifierFooService.class, FooServiceImpl.class, OtherFooService.class, ScopedProxyTestBean.class); } @Test @@ -237,7 +239,8 @@ class ClassPathScanningCandidateComponentProviderTests { provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); + assertBeanTypes(candidates, + NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); } @Test @@ -282,7 +285,8 @@ class ClassPathScanningCandidateComponentProviderTests { private void testExclude(ClassPathScanningCandidateComponentProvider provider) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); - assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class, + assertBeanTypes(candidates, + FooServiceImpl.class, OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class); } @@ -301,7 +305,8 @@ class ClassPathScanningCandidateComponentProviderTests { provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); + assertBeanTypes(candidates, + NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); } @Test @@ -334,8 +339,9 @@ class ClassPathScanningCandidateComponentProviderTests { provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class, - BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); + assertBeanTypes(candidates, + DefaultNamedComponent.class, FooServiceImpl.class, NamedComponent.class, NamedStubDao.class, + OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class); } @Test @@ -345,8 +351,9 @@ class ClassPathScanningCandidateComponentProviderTests { provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); provider.addExcludeFilter(new AssignableTypeFilter(FooService.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class, - DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); + assertBeanTypes(candidates, + DefaultNamedComponent.class, NamedComponent.class, NamedStubDao.class, + ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java index 556d0b91fa5..1f758bfff60 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java @@ -47,6 +47,7 @@ class EnableAspectJAutoProxyTests { aspectIsApplied(ctx); assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue(); ctx.close(); } @@ -56,6 +57,7 @@ class EnableAspectJAutoProxyTests { aspectIsApplied(ctx); assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue(); ctx.close(); } @@ -124,7 +126,7 @@ class EnableAspectJAutoProxyTests { } - @Import({ ServiceInvocationCounter.class, StubFooDao.class }) + @Import({ServiceInvocationCounter.class, StubFooDao.class}) @EnableAspectJAutoProxy(exposeProxy = true) static class ConfigWithExposedProxy { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java index 884b830695f..867a4847932 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java @@ -22,9 +22,13 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -32,10 +36,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Proxyable; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.context.annotation.ProxyType.INTERFACES; +import static org.springframework.context.annotation.ProxyType.TARGET_CLASS; /** * System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes. @@ -62,18 +69,40 @@ class ConfigurationClassAspectIntegrationTests { assertAdviceWasApplied(ConfigurationWithAspect.class); } - private void assertAdviceWasApplied(Class configClass) { + @Test + void configurationIncludesAspectAndProxyable() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyable.class, TestBean.class); + } + + @Test + void configurationIncludesAspectAndProxyableInterfaces() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyableInterfaces.class, TestBean.class, Comparable.class); + } + + @Test + void configurationIncludesAspectAndProxyableTargetClass() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyableTargetClass.class); + } + + private void assertAdviceWasApplied(Class configClass, Class... notImplemented) { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class)); GenericApplicationContext ctx = new GenericApplicationContext(factory); ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor()); - ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + ctx.registerBeanDefinition("config", + new RootBeanDefinition(configClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); ctx.refresh(); - TestBean testBean = ctx.getBean("testBean", TestBean.class); + ITestBean testBean = ctx.getBean("testBean", ITestBean.class); + if (notImplemented.length > 0) { + assertThat(testBean).isNotInstanceOfAny(notImplemented); + } + else { + assertThat(testBean).isInstanceOf(TestBean.class); + } assertThat(testBean.getName()).isEqualTo("name"); - testBean.absquatulate(); + ((IOther) testBean).absquatulate(); assertThat(testBean.getName()).isEqualTo("advisedName"); ctx.close(); } @@ -120,6 +149,58 @@ class ConfigurationClassAspectIntegrationTests { } + @Configuration + static class ConfigurationWithAspectAndProxyable { + + @Bean + @Proxyable(INTERFACES) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + + @Configuration() + static class ConfigurationWithAspectAndProxyableInterfaces { + + @Bean + @Proxyable(interfaces = {ITestBean.class, IOther.class}) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + + @Configuration + static class ConfigurationWithAspectAndProxyableTargetClass { + + public ConfigurationWithAspectAndProxyableTargetClass(AbstractAutoProxyCreator autoProxyCreator) { + autoProxyCreator.setProxyTargetClass(false); + } + + @Bean + @Proxyable(TARGET_CLASS) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + @Aspect static class NameChangingAspect { diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components index 8c298cd44d9..c56e0854cb8 100644 --- a/spring-context/src/test/resources/example/scannable/spring.components +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -1,12 +1,13 @@ example.scannable.AutowiredQualifierFooService=example.scannable.FooService example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component -example.scannable.NamedComponent=org.springframework.stereotype.Component example.scannable.FooService=example.scannable.FooService example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService -example.scannable.ScopedProxyTestBean=example.scannable.FooService -example.scannable.StubFooDao=org.springframework.stereotype.Component +example.scannable.NamedComponent=org.springframework.stereotype.Component example.scannable.NamedStubDao=org.springframework.stereotype.Component +example.scannable.OtherFooService=org.springframework.stereotype.Component,example.scannable.FooService +example.scannable.ScopedProxyTestBean=example.scannable.FooService example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component +example.scannable.StubFooDao=org.springframework.stereotype.Component example.scannable.sub.BarComponent=org.springframework.stereotype.Component example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.scannable.JakartaNamedComponent=jakarta.inject.Named