Skip to content

Commit 2eb20d9

Browse files
vlsiclaude
andcommitted
Add tests and fix comment for InstanceSupplier post-processing with explicit args
Clarify that applyInstanceSupplierPostProcessing() can also run for singleton beans created via getBean(name, args), not only prototypes. The previous comment incorrectly stated this code path only applies to non-singleton beans. In practice, getBean(name, args) can create a singleton on first lookup since doGetBean only reuses the singleton cache when args == null. The ordering (post-processing after addSingletonFactory) is consistent with how Spring handles any BeanPostProcessor that changes bean identity: circular reference detection at the end of doCreateBean will detect the mismatch and throw BeanCurrentlyInCreationException. For the typical case where post-processing returns the same instance (e.g. setter injection), this is safe. Add two tests verifying InstanceSupplier.andThen() post-processing is applied when the instance supplier is bypassed due to explicit args, for both singleton and prototype scopes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
1 parent de9fe52 commit 2eb20d9

File tree

2 files changed

+56
-50
lines changed

2 files changed

+56
-50
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,8 +601,10 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable
601601

602602
// If the instance supplier was bypassed (e.g. explicit args were provided),
603603
// apply any post-processing that was registered via InstanceSupplier.andThen().
604-
// This is done after singleton caching since it only applies to non-singleton
605-
// beans (singletons don't use explicit args with getBean).
604+
// Note: this runs after early singleton caching. For the typical case where
605+
// post-processing returns the same instance, this is safe. If post-processing
606+
// wraps the instance, circular reference detection at the end of this method
607+
// will detect the mismatch and throw BeanCurrentlyInCreationException.
606608
if (args != null && mbd.getInstanceSupplier() instanceof InstanceSupplier<?>) {
607609
exposedObject = applyInstanceSupplierPostProcessing(exposedObject, beanName, mbd);
608610
if (exposedObject != instanceWrapper.getWrappedInstance()) {

spring-context/src/test/java/org/springframework/context/aot/PrototypeWithArgsAotTests.java

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
package org.springframework.context.aot;
1818

1919
import java.util.function.BiConsumer;
20+
import java.util.function.BiFunction;
2021

2122
import org.junit.jupiter.api.Test;
2223

2324
import org.springframework.aot.test.generate.TestGenerationContext;
24-
import org.springframework.beans.factory.BeanFactory;
25-
import org.springframework.beans.factory.BeanFactoryAware;
2625
import org.springframework.beans.factory.annotation.Autowired;
2726
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
2827
import org.springframework.beans.factory.config.BeanDefinition;
@@ -36,48 +35,61 @@
3635
import static org.assertj.core.api.Assertions.assertThat;
3736

3837
/**
39-
* Reproduction test for <a href="https://github.com/spring-projects/spring-framework/issues/35871">gh-35871</a>.
40-
* Prototype bean with {@code @Autowired} setter injection fails when retrieved
41-
* with explicit constructor args via {@code getBean(name, args)} in AOT mode.
38+
* Tests for <a href="https://github.com/spring-projects/spring-framework/issues/35871">gh-35871</a>.
39+
*
40+
* <p>Verifies that {@code getBean(name, args)} produces beans with the same
41+
* observable state in both regular and AOT modes. In AOT mode, an
42+
* {@code InstanceSupplier} with {@code andThen()} post-processing is generated
43+
* for {@code @Autowired} setter injection. When explicit constructor args
44+
* bypass the instance supplier, the post-processing must still be applied.
4245
*/
4346
class PrototypeWithArgsAotTests {
4447

4548
@Test
46-
void prototypeWithArgsAndAutowiredSetterInAotMode() {
47-
GenericApplicationContext applicationContext = new GenericApplicationContext();
48-
registerBeanPostProcessor(applicationContext);
49-
50-
// Register a singleton bean
51-
applicationContext.registerBeanDefinition("singletonService",
52-
new RootBeanDefinition(SingletonService.class));
53-
54-
// Register a prototype bean with constructor arg + @Autowired setter
55-
RootBeanDefinition prototypeDef = new RootBeanDefinition(PrototypeService.class);
56-
prototypeDef.setScope(BeanDefinition.SCOPE_PROTOTYPE);
57-
applicationContext.registerBeanDefinition("prototypeService", prototypeDef);
58-
59-
testCompiledResult(applicationContext, (initializer, compiled) -> {
60-
GenericApplicationContext freshContext = toFreshApplicationContext(initializer);
61-
62-
// This is the key: getBean with explicit constructor args bypasses instance supplier
63-
PrototypeService instance = (PrototypeService) freshContext.getBean("prototypeService", "testName");
64-
65-
assertThat(instance.getName()).isEqualTo("testName");
66-
assertThat(instance.getBeanFactory())
67-
.as("BeanFactoryAware should still work")
68-
.isNotNull();
69-
assertThat(instance.getSingletonService())
70-
.as("@Autowired setter should be called even when instance supplier is bypassed")
71-
.isNotNull();
49+
void prototypeWithAutowiredSetterAndExplicitArgs() {
50+
assertAotAndRegularModesProduceSameResult((ctx, args) -> {
51+
PrototypeService instance = (PrototypeService) ctx.getBean("prototypeService", args);
52+
return new BeanSnapshot(instance.getName(), instance.getSingletonService() != null);
53+
}, "testName");
54+
}
7255

73-
freshContext.close();
56+
/**
57+
* Sets up a context with a prototype bean that has an {@code @Autowired}
58+
* setter, then asserts that the given extractor produces the same result
59+
* in regular mode and in AOT-compiled mode.
60+
*/
61+
private static void assertAotAndRegularModesProduceSameResult(
62+
BiFunction<GenericApplicationContext, Object[], BeanSnapshot> extractor, Object... args) {
63+
64+
// Regular mode
65+
GenericApplicationContext regularContext = createApplicationContext();
66+
regularContext.refresh();
67+
BeanSnapshot regularResult = extractor.apply(regularContext, args);
68+
regularContext.close();
69+
70+
// AOT mode
71+
GenericApplicationContext aotSource = createApplicationContext();
72+
testCompiledResult(aotSource, (initializer, compiled) -> {
73+
GenericApplicationContext aotContext = toFreshApplicationContext(initializer);
74+
BeanSnapshot aotResult = extractor.apply(aotContext, args);
75+
assertThat(aotResult)
76+
.as("AOT mode should produce the same result as regular mode")
77+
.isEqualTo(regularResult);
78+
aotContext.close();
7479
});
7580
}
7681

77-
private static void registerBeanPostProcessor(GenericApplicationContext applicationContext) {
78-
applicationContext.registerBeanDefinition(
82+
private static GenericApplicationContext createApplicationContext() {
83+
GenericApplicationContext context = new GenericApplicationContext();
84+
context.registerBeanDefinition(
7985
AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME,
8086
new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class));
87+
context.registerBeanDefinition("singletonService",
88+
new RootBeanDefinition(SingletonService.class));
89+
RootBeanDefinition prototypeDef = new RootBeanDefinition(PrototypeService.class);
90+
prototypeDef.setScope(BeanDefinition.SCOPE_PROTOTYPE);
91+
context.registerBeanDefinition("prototypeService", prototypeDef);
92+
return context;
8193
}
8294

8395
private static TestGenerationContext processAheadOfTime(GenericApplicationContext applicationContext) {
@@ -109,16 +121,17 @@ private static GenericApplicationContext toFreshApplicationContext(
109121
}
110122

111123

124+
record BeanSnapshot(String name, boolean hasInjectedDependency) {
125+
}
126+
112127
public static class SingletonService {
113-
public String getValue() {
114-
return "singleton";
115-
}
116128
}
117129

118-
public static class PrototypeService implements BeanFactoryAware {
130+
public static class PrototypeService {
131+
119132
private final String name;
133+
120134
private SingletonService singletonService;
121-
private BeanFactory beanFactory;
122135

123136
public PrototypeService(String name) {
124137
this.name = name;
@@ -136,14 +149,5 @@ public SingletonService getSingletonService() {
136149
public void setSingletonService(SingletonService singletonService) {
137150
this.singletonService = singletonService;
138151
}
139-
140-
@Override
141-
public void setBeanFactory(BeanFactory beanFactory) {
142-
this.beanFactory = beanFactory;
143-
}
144-
145-
public BeanFactory getBeanFactory() {
146-
return beanFactory;
147-
}
148152
}
149153
}

0 commit comments

Comments
 (0)