Skip to content

Commit ecff60c

Browse files
vlsiclaude
andcommitted
Apply InstanceSupplier post-processing when bypassed by explicit args
When getBean(name, args) is called with explicit constructor arguments, the InstanceSupplier is intentionally bypassed (gh-32657). However, in AOT mode, @Autowired setter/field injection is baked into the InstanceSupplier's andThen() chain and AutowiredAnnotationBeanPostProcessor is excluded from runtime registration. This means the autowiring post-processing is lost when the supplier is bypassed. Add InstanceSupplier.postProcessInstance() to allow applying only the post-processing steps (from andThen()) to an already-created instance without re-invoking creation. Call this from doCreateBean() when the instance supplier was bypassed due to explicit args. Closes gh-35871 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
1 parent 227bc19 commit ecff60c

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,20 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable
598598

599599
// Initialize the bean instance.
600600
Object exposedObject = bean;
601+
602+
// If the instance supplier was bypassed (e.g. explicit args were provided),
603+
// apply any post-processing that was registered via InstanceSupplier.andThen().
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.
608+
if (args != null && mbd.getInstanceSupplier() instanceof InstanceSupplier<?>) {
609+
exposedObject = applyInstanceSupplierPostProcessing(exposedObject, beanName, mbd);
610+
if (exposedObject != instanceWrapper.getWrappedInstance()) {
611+
instanceWrapper = new BeanWrapperImpl(exposedObject);
612+
initBeanWrapper(instanceWrapper);
613+
}
614+
}
601615
try {
602616
populateBean(beanName, mbd, instanceWrapper);
603617
exposedObject = initializeBean(beanName, exposedObject, mbd);
@@ -1285,6 +1299,23 @@ private BeanWrapper obtainFromSupplier(Supplier<?> supplier, String beanName, Ro
12851299
return supplier.get();
12861300
}
12871301

1302+
/**
1303+
* Apply any post-processing from the bean definition's
1304+
* {@link InstanceSupplier} to an already-created instance. This is called
1305+
* when the instance supplier was bypassed during creation (for example,
1306+
* when explicit constructor arguments were provided) but the post-processing
1307+
* registered via {@link InstanceSupplier#andThen} still needs to be applied.
1308+
* @param bean the already-created bean instance
1309+
* @param beanName the name of the bean
1310+
* @param mbd the bean definition for the bean
1311+
* @return the post-processed bean instance
1312+
* @since 7.0
1313+
* @see InstanceSupplier#postProcessInstance
1314+
*/
1315+
protected Object applyInstanceSupplierPostProcessing(Object bean, String beanName, RootBeanDefinition mbd) {
1316+
return bean;
1317+
}
1318+
12881319
/**
12891320
* Overridden in order to implicitly register the currently created bean as
12901321
* dependent on further beans getting programmatically retrieved during a

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,26 @@ protected boolean isBeanEligibleForMetadataCaching(String beanName) {
10261026
return super.obtainInstanceFromSupplier(supplier, beanName, mbd);
10271027
}
10281028

1029+
@Override
1030+
@SuppressWarnings("unchecked")
1031+
protected Object applyInstanceSupplierPostProcessing(Object bean, String beanName, RootBeanDefinition mbd) {
1032+
InstanceSupplier<?> instanceSupplier = (InstanceSupplier<?>) mbd.getInstanceSupplier();
1033+
if (instanceSupplier != null) {
1034+
try {
1035+
return ((InstanceSupplier<Object>) instanceSupplier)
1036+
.postProcessInstance(RegisteredBean.of(this, beanName, mbd), bean);
1037+
}
1038+
catch (RuntimeException ex) {
1039+
throw ex;
1040+
}
1041+
catch (Exception ex) {
1042+
throw new BeanCreationException(beanName,
1043+
"Post-processing of instance supplier failed", ex);
1044+
}
1045+
}
1046+
return bean;
1047+
}
1048+
10291049
@Override
10301050
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
10311051
super.cacheMergedBeanDefinition(mbd, beanName);

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ default T getWithException() {
6464
return null;
6565
}
6666

67+
/**
68+
* Apply only the post-processing steps of this supplier to an
69+
* already-created instance, without invoking the instance creation itself.
70+
* <p>This is used when the instance was created through a different path
71+
* (for example, when explicit constructor arguments bypass the instance
72+
* supplier) but post-processing registered via {@link #andThen} still
73+
* needs to be applied.
74+
* @param registeredBean the registered bean
75+
* @param instance the already-created instance to post-process
76+
* @return the post-processed instance
77+
* @throws Exception on error
78+
* @since 7.0
79+
* @see #andThen
80+
*/
81+
@SuppressWarnings("unchecked")
82+
default T postProcessInstance(RegisteredBean registeredBean, T instance) throws Exception {
83+
return instance;
84+
}
85+
6786
/**
6887
* Return a composed instance supplier that first obtains the instance from
6988
* this supplier and then applies the {@code after} function to obtain the
@@ -83,6 +102,12 @@ public V get(RegisteredBean registeredBean) throws Exception {
83102
return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean));
84103
}
85104
@Override
105+
@SuppressWarnings("unchecked")
106+
public V postProcessInstance(RegisteredBean registeredBean, V instance) throws Exception {
107+
T postProcessed = InstanceSupplier.this.postProcessInstance(registeredBean, (T) instance);
108+
return after.applyWithException(registeredBean, postProcessed);
109+
}
110+
@Override
86111
public @Nullable Method getFactoryMethod() {
87112
return InstanceSupplier.this.getFactoryMethod();
88113
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 org.springframework.context.aot;
18+
19+
import java.util.function.BiConsumer;
20+
import java.util.function.BiFunction;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.aot.test.generate.TestGenerationContext;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
27+
import org.springframework.beans.factory.config.BeanDefinition;
28+
import org.springframework.beans.factory.support.RootBeanDefinition;
29+
import org.springframework.context.ApplicationContextInitializer;
30+
import org.springframework.context.annotation.AnnotationConfigUtils;
31+
import org.springframework.context.support.GenericApplicationContext;
32+
import org.springframework.core.test.tools.Compiled;
33+
import org.springframework.core.test.tools.TestCompiler;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
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.
45+
*/
46+
class PrototypeWithArgsAotTests {
47+
48+
@Test
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+
}
55+
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();
79+
});
80+
}
81+
82+
private static GenericApplicationContext createApplicationContext() {
83+
GenericApplicationContext context = new GenericApplicationContext();
84+
context.registerBeanDefinition(
85+
AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME,
86+
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;
93+
}
94+
95+
private static TestGenerationContext processAheadOfTime(GenericApplicationContext applicationContext) {
96+
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
97+
TestGenerationContext generationContext = new TestGenerationContext();
98+
generator.processAheadOfTime(applicationContext, generationContext);
99+
generationContext.writeGeneratedContent();
100+
return generationContext;
101+
}
102+
103+
private static void testCompiledResult(GenericApplicationContext applicationContext,
104+
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
105+
testCompiledResult(processAheadOfTime(applicationContext), result);
106+
}
107+
108+
@SuppressWarnings("unchecked")
109+
private static void testCompiledResult(TestGenerationContext generationContext,
110+
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
111+
TestCompiler.forSystem().with(generationContext).compile(compiled ->
112+
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
113+
}
114+
115+
private static GenericApplicationContext toFreshApplicationContext(
116+
ApplicationContextInitializer<GenericApplicationContext> initializer) {
117+
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
118+
initializer.initialize(freshApplicationContext);
119+
freshApplicationContext.refresh();
120+
return freshApplicationContext;
121+
}
122+
123+
124+
record BeanSnapshot(String name, boolean hasInjectedDependency) {
125+
}
126+
127+
public static class SingletonService {
128+
}
129+
130+
public static class PrototypeService {
131+
132+
private final String name;
133+
134+
private SingletonService singletonService;
135+
136+
public PrototypeService(String name) {
137+
this.name = name;
138+
}
139+
140+
public String getName() {
141+
return name;
142+
}
143+
144+
public SingletonService getSingletonService() {
145+
return singletonService;
146+
}
147+
148+
@Autowired
149+
public void setSingletonService(SingletonService singletonService) {
150+
this.singletonService = singletonService;
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)