Skip to content

Commit 1e1a7c0

Browse files
committed
add a bug fix and a test case for #11656
1 parent c97bfac commit 1e1a7c0

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
import java.util.Objects;
132132
import java.util.Optional;
133133
import java.util.Set;
134+
import java.util.WeakHashMap;
134135
import java.util.concurrent.ConcurrentHashMap;
135136
import java.util.concurrent.CopyOnWriteArrayList;
136137
import java.util.concurrent.ForkJoinPool;
@@ -175,6 +176,13 @@ public class DefaultBeanContext implements InitializableBeanContext, Configurabl
175176

176177
protected final SingletonScope singletonScope = new SingletonScope();
177178

179+
/**
180+
* Track instances created by this context so that if the same instance is later
181+
* registered via registerSingleton(...) we can avoid duplicate injection/initialization.
182+
* Use a WeakHashMap-backed set so instances can be GC'ed and to avoid leaks.
183+
*/
184+
private final Set<Object> beansCreatedByContext = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
185+
178186
private final BeanContextConfiguration beanContextConfiguration;
179187

180188
// The collection should be modified only when new bean definition is added
@@ -691,7 +699,19 @@ public <T> BeanContext registerSingleton(@NonNull Class<T> type, @NonNull T sing
691699
if (beanDefinition != null && !(beanDefinition instanceof RuntimeBeanDefinition<T>) && beanDefinition.getBeanType().isInstance(singleton)) {
692700
try (BeanResolutionContext context = newResolutionContext(beanDefinition, null)) {
693701
if (inject) {
694-
doInjectAndInitialize(context, singleton, beanDefinition);
702+
// If this instance was created by this context earlier, it may already have been injected/initialized.
703+
// In such case skip duplicate injection/initialization. Use the beansCreatedByContext set to detect this.
704+
boolean createdByThisContext;
705+
synchronized (beansCreatedByContext) {
706+
createdByThisContext = beansCreatedByContext.remove(singleton);
707+
}
708+
if (!createdByThisContext) {
709+
doInjectAndInitialize(context, singleton, beanDefinition);
710+
} else {
711+
if (LOG_LIFECYCLE.isDebugEnabled()) {
712+
LOG_LIFECYCLE.debug("Skipping duplicate injection/initialization for bean [{}] as it was created by the context", singleton);
713+
}
714+
}
695715
}
696716
DefaultBeanContext.BeanKey<T> key = new DefaultBeanContext.BeanKey<>(beanDefinition.asArgument(), qualifier);
697717
singletonScope.registerSingletonBean(BeanRegistration.of(this, key, beanDefinition, singleton), qualifier);
@@ -2349,6 +2369,11 @@ private <T> T resolveByBeanFactory(@NonNull BeanResolutionContext resolutionCont
23492369
if (bean instanceof Qualified qualified) {
23502370
qualified.$withBeanQualifier(declaredQualifier);
23512371
}
2372+
2373+
// Track instances created by this context so registerSingleton can avoid
2374+
// doing duplicate injection/initialization when someone registers the same instance.
2375+
beansCreatedByContext.add(bean);
2376+
23522377
return bean;
23532378
} catch (DependencyInjectionException | DisabledBeanException | BeanInstantiationException e) {
23542379
throw e;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.micronaut.reproduce;
2+
3+
import io.micronaut.context.ApplicationContext;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertSame;
8+
9+
public class DoublePostConstructTest {
10+
11+
@Test
12+
void postConstructCalledOnceWhenCreateAndRegisterSingleton() {
13+
try (ApplicationContext ctx = ApplicationContext.run()) {
14+
// createBean should call @PostConstruct once
15+
MyBean bean = ctx.createBean(MyBean.class);
16+
assertEquals(1, bean.getInitCount(), "@PostConstruct should have been called once after createBean");
17+
18+
// registering the already-created instance must NOT call @PostConstruct again
19+
ctx.registerSingleton(bean);
20+
21+
MyBean resolved = ctx.getBean(MyBean.class);
22+
assertSame(bean, resolved, "Registered singleton should be the same instance");
23+
// Expected: still 1. If bug present, this will be 2.
24+
assertEquals(1, bean.getInitCount(), "@PostConstruct was called more than once when registering singleton");
25+
}
26+
}
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.micronaut.reproduce;
2+
3+
import jakarta.annotation.PostConstruct;
4+
import jakarta.inject.Singleton;
5+
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
@Singleton
9+
public class MyBean {
10+
11+
private final AtomicInteger initCount = new AtomicInteger(0);
12+
13+
@PostConstruct
14+
void init() {
15+
initCount.incrementAndGet();
16+
System.out.println("MyBean init called, count=" + initCount.get() + " instance=" + this);
17+
}
18+
19+
public String hello() {
20+
return "hello";
21+
}
22+
23+
public int getInitCount() {
24+
return initCount.get();
25+
}
26+
}

0 commit comments

Comments
 (0)