Skip to content

Commit df86a99

Browse files
committed
Introduce @proxyable annotation for bean-specific proxy type
Closes gh-35296 See gh-35293
1 parent 9edb96a commit df86a99

File tree

8 files changed

+175
-22
lines changed

8 files changed

+175
-22
lines changed

spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
464464
proxyFactory.addInterface(ifc);
465465
}
466466
}
467-
else if (!proxyFactory.isProxyTargetClass()) {
467+
if (ifcs != null ? ifcs.length == 0 : !proxyFactory.isProxyTargetClass()) {
468468
evaluateProxyInterfaces(beanClass, proxyFactory);
469469
}
470470
}

spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.jspecify.annotations.Nullable;
2424

25+
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
2526
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
2627
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
2728
import org.springframework.beans.factory.config.BeanDefinition;
@@ -258,6 +259,20 @@ else if (abd.getMetadata() != metadata) {
258259
if (description != null) {
259260
abd.setDescription(description.getString("value"));
260261
}
262+
263+
AnnotationAttributes proxyable = attributesFor(metadata, Proxyable.class);
264+
if (proxyable != null) {
265+
ProxyType mode = proxyable.getEnum("value");
266+
if (mode == ProxyType.TARGET_CLASS) {
267+
abd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
268+
}
269+
else {
270+
Class<?>[] ifcs = proxyable.getClassArray("interfaces");
271+
if (ifcs.length > 0 || mode == ProxyType.INTERFACES) {
272+
abd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, ifcs);
273+
}
274+
}
275+
}
261276
}
262277

263278
static BeanDefinitionHolder applyScopedProxyMode(

spring-context/src/test/java/example/scannable/FooServiceImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.context.MessageSource;
3434
import org.springframework.context.annotation.DependsOn;
3535
import org.springframework.context.annotation.Lazy;
36+
import org.springframework.context.annotation.Primary;
3637
import org.springframework.context.support.AbstractApplicationContext;
3738
import org.springframework.core.io.ResourceLoader;
3839
import org.springframework.core.io.support.ResourcePatternResolver;
@@ -43,7 +44,7 @@
4344
* @author Mark Fisher
4445
* @author Juergen Hoeller
4546
*/
46-
@Service @Lazy @DependsOn("myNamedComponent")
47+
@Service @Primary @Lazy @DependsOn("myNamedComponent")
4748
public abstract class FooServiceImpl implements FooService {
4849

4950
// Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package example.scannable;
18+
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.Future;
21+
22+
import org.springframework.context.annotation.Proxyable;
23+
import org.springframework.stereotype.Service;
24+
25+
/**
26+
* @author Juergen Hoeller
27+
*/
28+
@Service @Proxyable(interfaces = FooService.class)
29+
public class OtherFooService implements FooService {
30+
31+
@Override
32+
public String foo(int id) {
33+
return "" + id;
34+
}
35+
36+
@Override
37+
public Future<String> asyncFoo(int id) {
38+
return CompletableFuture.completedFuture("" + id);
39+
}
40+
41+
@Override
42+
public boolean isInitCalled() {
43+
return false;
44+
}
45+
46+
}

spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import example.scannable.MessageBean;
4242
import example.scannable.NamedComponent;
4343
import example.scannable.NamedStubDao;
44+
import example.scannable.OtherFooService;
4445
import example.scannable.ScopedProxyTestBean;
4546
import example.scannable.ServiceInvocationCounter;
4647
import example.scannable.StubFooDao;
@@ -85,13 +86,13 @@ class ClassPathScanningCandidateComponentProviderTests {
8586

8687
private static final Set<Class<?>> springComponents = Set.of(
8788
DefaultNamedComponent.class,
88-
NamedComponent.class,
8989
FooServiceImpl.class,
90-
StubFooDao.class,
90+
NamedComponent.class,
9191
NamedStubDao.class,
92+
OtherFooService.class,
9293
ServiceInvocationCounter.class,
93-
BarComponent.class
94-
);
94+
StubFooDao.class,
95+
BarComponent.class);
9596

9697

9798
@Test
@@ -213,7 +214,8 @@ private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateCom
213214
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
214215
assertScannedBeanDefinitions(candidates);
215216
// Interfaces/Abstract class are filtered out automatically.
216-
assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class);
217+
assertBeanTypes(candidates,
218+
AutowiredQualifierFooService.class, FooServiceImpl.class, OtherFooService.class, ScopedProxyTestBean.class);
217219
}
218220

219221
@Test
@@ -237,7 +239,8 @@ private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandida
237239
provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class));
238240
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
239241
assertScannedBeanDefinitions(candidates);
240-
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
242+
assertBeanTypes(candidates,
243+
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
241244
}
242245

243246
@Test
@@ -282,7 +285,8 @@ void excludeFilterWithIndex() {
282285
private void testExclude(ClassPathScanningCandidateComponentProvider provider) {
283286
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
284287
assertScannedBeanDefinitions(candidates);
285-
assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class,
288+
assertBeanTypes(candidates,
289+
FooServiceImpl.class, OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class,
286290
BarComponent.class);
287291
}
288292

@@ -301,7 +305,8 @@ void withComponentAnnotationOnly() {
301305
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
302306
provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class));
303307
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
304-
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
308+
assertBeanTypes(candidates,
309+
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
305310
}
306311

307312
@Test
@@ -334,8 +339,9 @@ void withMultipleMatchingFilters() {
334339
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
335340
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
336341
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
337-
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class,
338-
BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
342+
assertBeanTypes(candidates,
343+
DefaultNamedComponent.class, FooServiceImpl.class, NamedComponent.class, NamedStubDao.class,
344+
OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
339345
}
340346

341347
@Test
@@ -345,8 +351,9 @@ void excludeTakesPrecedence() {
345351
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
346352
provider.addExcludeFilter(new AssignableTypeFilter(FooService.class));
347353
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
348-
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class,
349-
DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
354+
assertBeanTypes(candidates,
355+
DefaultNamedComponent.class, NamedComponent.class, NamedStubDao.class,
356+
ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
350357
}
351358

352359
@Test

spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ void withJdkProxy() {
4747

4848
aspectIsApplied(ctx);
4949
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue();
50+
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
5051
ctx.close();
5152
}
5253

@@ -56,6 +57,7 @@ void withCglibProxy() {
5657

5758
aspectIsApplied(ctx);
5859
assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue();
60+
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
5961
ctx.close();
6062
}
6163

@@ -124,7 +126,7 @@ static class ConfigWithCglibProxy {
124126
}
125127

126128

127-
@Import({ ServiceInvocationCounter.class, StubFooDao.class })
129+
@Import({ServiceInvocationCounter.class, StubFooDao.class})
128130
@EnableAspectJAutoProxy(exposeProxy = true)
129131
static class ConfigWithExposedProxy {
130132

spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,27 @@
2222
import org.aspectj.lang.annotation.Before;
2323
import org.junit.jupiter.api.Test;
2424

25+
import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator;
26+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2527
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2628
import org.springframework.beans.factory.support.RootBeanDefinition;
2729
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
30+
import org.springframework.beans.testfixture.beans.IOther;
31+
import org.springframework.beans.testfixture.beans.ITestBean;
2832
import org.springframework.beans.testfixture.beans.TestBean;
2933
import org.springframework.context.ConfigurableApplicationContext;
3034
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3135
import org.springframework.context.annotation.Bean;
3236
import org.springframework.context.annotation.Configuration;
3337
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
3438
import org.springframework.context.annotation.EnableAspectJAutoProxy;
39+
import org.springframework.context.annotation.Proxyable;
3540
import org.springframework.context.support.GenericApplicationContext;
3641
import org.springframework.core.io.ClassPathResource;
3742

3843
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.springframework.context.annotation.ProxyType.INTERFACES;
45+
import static org.springframework.context.annotation.ProxyType.TARGET_CLASS;
3946

4047
/**
4148
* System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes.
@@ -62,18 +69,40 @@ void configurationIncludesAspect() {
6269
assertAdviceWasApplied(ConfigurationWithAspect.class);
6370
}
6471

65-
private void assertAdviceWasApplied(Class<?> configClass) {
72+
@Test
73+
void configurationIncludesAspectAndProxyable() {
74+
assertAdviceWasApplied(ConfigurationWithAspectAndProxyable.class, TestBean.class);
75+
}
76+
77+
@Test
78+
void configurationIncludesAspectAndProxyableInterfaces() {
79+
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableInterfaces.class, TestBean.class, Comparable.class);
80+
}
81+
82+
@Test
83+
void configurationIncludesAspectAndProxyableTargetClass() {
84+
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableTargetClass.class);
85+
}
86+
87+
private void assertAdviceWasApplied(Class<?> configClass, Class<?>... notImplemented) {
6688
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
6789
new XmlBeanDefinitionReader(factory).loadBeanDefinitions(
6890
new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class));
6991
GenericApplicationContext ctx = new GenericApplicationContext(factory);
7092
ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor());
71-
ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass));
93+
ctx.registerBeanDefinition("config",
94+
new RootBeanDefinition(configClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false));
7295
ctx.refresh();
7396

74-
TestBean testBean = ctx.getBean("testBean", TestBean.class);
97+
ITestBean testBean = ctx.getBean("testBean", ITestBean.class);
98+
if (notImplemented.length > 0) {
99+
assertThat(testBean).isNotInstanceOfAny(notImplemented);
100+
}
101+
else {
102+
assertThat(testBean).isInstanceOf(TestBean.class);
103+
}
75104
assertThat(testBean.getName()).isEqualTo("name");
76-
testBean.absquatulate();
105+
((IOther) testBean).absquatulate();
77106
assertThat(testBean.getName()).isEqualTo("advisedName");
78107
ctx.close();
79108
}
@@ -120,6 +149,58 @@ public NameChangingAspect nameChangingAspect() {
120149
}
121150

122151

152+
@Configuration
153+
static class ConfigurationWithAspectAndProxyable {
154+
155+
@Bean
156+
@Proxyable(INTERFACES)
157+
public TestBean testBean() {
158+
return new TestBean("name");
159+
}
160+
161+
@Bean
162+
public NameChangingAspect nameChangingAspect() {
163+
return new NameChangingAspect();
164+
}
165+
}
166+
167+
168+
@Configuration()
169+
static class ConfigurationWithAspectAndProxyableInterfaces {
170+
171+
@Bean
172+
@Proxyable(interfaces = {ITestBean.class, IOther.class})
173+
public TestBean testBean() {
174+
return new TestBean("name");
175+
}
176+
177+
@Bean
178+
public NameChangingAspect nameChangingAspect() {
179+
return new NameChangingAspect();
180+
}
181+
}
182+
183+
184+
@Configuration
185+
static class ConfigurationWithAspectAndProxyableTargetClass {
186+
187+
public ConfigurationWithAspectAndProxyableTargetClass(AbstractAutoProxyCreator autoProxyCreator) {
188+
autoProxyCreator.setProxyTargetClass(false);
189+
}
190+
191+
@Bean
192+
@Proxyable(TARGET_CLASS)
193+
public TestBean testBean() {
194+
return new TestBean("name");
195+
}
196+
197+
@Bean
198+
public NameChangingAspect nameChangingAspect() {
199+
return new NameChangingAspect();
200+
}
201+
}
202+
203+
123204
@Aspect
124205
static class NameChangingAspect {
125206

spring-context/src/test/resources/example/scannable/spring.components

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
example.scannable.AutowiredQualifierFooService=example.scannable.FooService
22
example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component
3-
example.scannable.NamedComponent=org.springframework.stereotype.Component
43
example.scannable.FooService=example.scannable.FooService
54
example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService
6-
example.scannable.ScopedProxyTestBean=example.scannable.FooService
7-
example.scannable.StubFooDao=org.springframework.stereotype.Component
5+
example.scannable.NamedComponent=org.springframework.stereotype.Component
86
example.scannable.NamedStubDao=org.springframework.stereotype.Component
7+
example.scannable.OtherFooService=org.springframework.stereotype.Component,example.scannable.FooService
8+
example.scannable.ScopedProxyTestBean=example.scannable.FooService
99
example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component
10+
example.scannable.StubFooDao=org.springframework.stereotype.Component
1011
example.scannable.sub.BarComponent=org.springframework.stereotype.Component
1112
example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean
1213
example.scannable.JakartaNamedComponent=jakarta.inject.Named

0 commit comments

Comments
 (0)