Skip to content

Commit 6fc9704

Browse files
committed
preventing unwanted eager instantiation and ensuring correct bean selection by exact definition
1 parent c97bfac commit 6fc9704

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

inject/src/main/java/io/micronaut/context/DefaultBeanContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,20 @@ protected void initializeContext(
20322032
throw new BeanInstantiationException(MSG_BEAN_DEFINITION + (ref == null ? "<ref discarded>" : ref.getName()) + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e);
20332033
}
20342034
}
2035+
// Prune eager beans that are replaced by any replacement bean in the application,
2036+
// regardless of the replacement scope.
2037+
List<BeanDefinition<Object>> replacementTypes = new ArrayList<>(4);
2038+
for (BeanDefinitionProducer producer : beanDefinitionsClasses) {
2039+
BeanDefinition<Object> def = producer.getDefinitionIfEnabled(this);
2040+
if (def != null && def.getAnnotationMetadata().hasStereotype(REPLACES_ANN)) {
2041+
replacementTypes.add(def);
2042+
}
2043+
}
2044+
if (!replacementTypes.isEmpty()) {
2045+
//noinspection unchecked,rawtypes
2046+
eagerInit.removeIf(def -> checkIfReplacementExists(null, (List) replacementTypes, def));
2047+
}
2048+
// Also apply local replacement filtering amongst eager beans themselves.
20352049
filterReplacedBeans(null, eagerInit);
20362050
OrderUtil.sortOrdered(eagerInit);
20372051
for (BeanDefinition eagerInitDefinition : eagerInit) {

inject/src/main/java/io/micronaut/context/SingletonScope.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ <T> BeanRegistration<T> findBeanRegistration(@NonNull BeanDefinition<T> beanDefi
248248
@Nullable Qualifier<T> qualifier) {
249249
BeanRegistration<T> beanRegistration = singletonByBeanDefinition.get(BeanDefinitionIdentity.of(beanDefinition));
250250
if (beanRegistration == null) {
251-
return findCachedSingletonBeanRegistration(beanType, qualifier);
251+
// Do not fall back to cached registration by type/qualifier; we must respect the exact definition.
252+
return null;
252253
}
253254
return beanRegistration;
254255
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.micronaut.context.replacement
2+
3+
import io.micronaut.context.ApplicationContext
4+
import io.micronaut.context.annotation.Context
5+
import io.micronaut.context.annotation.Replaces
6+
import io.micronaut.context.annotation.Requires
7+
import jakarta.inject.Singleton
8+
import org.junit.jupiter.api.Assertions.*
9+
import org.junit.jupiter.api.Test
10+
import java.util.concurrent.atomic.AtomicInteger
11+
12+
interface ReplacedApi
13+
14+
@Context
15+
@Requires(property = "spec.name", value = "ContextBeanReplacementScopeTest")
16+
open class OriginalContextApi : ReplacedApi {
17+
companion object {
18+
@JvmField
19+
val created = AtomicInteger(0)
20+
}
21+
init {
22+
created.incrementAndGet()
23+
}
24+
}
25+
26+
@Singleton
27+
@Replaces(OriginalContextApi::class)
28+
@Requires(property = "spec.name", value = "ContextBeanReplacementScopeTest")
29+
open class ReplacementSingletonApi : ReplacedApi {
30+
companion object {
31+
@JvmField
32+
val created = AtomicInteger(0)
33+
}
34+
init {
35+
created.incrementAndGet()
36+
}
37+
}
38+
39+
class ContextBeanReplacementScopeTest {
40+
41+
@Test
42+
fun replacingContextBeanWithSingletonShouldPreventOriginalInstantiation() {
43+
// Reset counters for isolation
44+
OriginalContextApi.created.set(0)
45+
ReplacementSingletonApi.created.set(0)
46+
47+
val ctx = ApplicationContext.run(mapOf("spec.name" to "ContextBeanReplacementScopeTest"))
48+
try {
49+
// Expected: Replacing a @Context bean with a non-@Context bean should prevent eager instantiation
50+
// Actual (bug): OriginalContextApi is still eagerly created at startup
51+
assertEquals(0, OriginalContextApi.created.get(), "Original @Context bean should not be instantiated when replaced by a non-context bean")
52+
53+
val api = ctx.getBean(ReplacedApi::class.java)
54+
assertTrue(api is ReplacementSingletonApi, "Injected bean should be the replacement")
55+
assertEquals(1, ReplacementSingletonApi.created.get(), "Replacement bean should be constructed once on demand")
56+
assertEquals(0, OriginalContextApi.created.get(), "Original bean must not be constructed at all")
57+
} finally {
58+
ctx.close()
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)