Introduce @Proxyable annotation for bean-specific proxy type

Closes gh-35296
See gh-35293
This commit is contained in:
Juergen Hoeller
2025-08-06 18:26:40 +02:00
parent 9edb96ae57
commit df86a9973d
8 changed files with 175 additions and 22 deletions
@@ -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);
}
}
@@ -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(
@@ -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
@@ -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<String> asyncFoo(int id) {
return CompletableFuture.completedFuture("" + id);
}
@Override
public boolean isInitCalled() {
return false;
}
}
@@ -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<Class<?>> 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<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> 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
@@ -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 {
@@ -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 {
@@ -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