diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index e1f60b9302ee..6464fdfff642 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -262,6 +262,24 @@ private Collection> retrieveApplicationListeners( if (supportsEvent(beanFactory, listenerBeanName, eventType)) { ApplicationListener listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class); + + // Despite best efforts to avoid it, unwrapped proxies (singleton targets) can end up in the + // list of programmatically registered listeners. In order to avoid duplicates, we need to find + // and replace them by their proxy counterparts, because if both a proxy and its target end up + // in 'allListeners', listeners will fire twice. + ApplicationListener unwrappedListener = + (ApplicationListener) AopProxyUtils.getSingletonTarget(listener); + if (listener != unwrappedListener) { + if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) { + filteredListeners.remove(unwrappedListener); + filteredListeners.add(listener); + } + if (allListeners.contains(unwrappedListener)) { + allListeners.remove(unwrappedListener); + allListeners.add(listener); + } + } + if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) { if (retriever != null) { if (beanFactory.isSingleton(listenerBeanName)) { diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index ea7e55cb4c03..6871fb3e8c91 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -39,6 +39,11 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.event.test.self_inject.MyApplication; +import org.springframework.context.event.test.self_inject.MyEventListener; +import org.springframework.context.event.test.self_inject.MyEventPublisher; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticMessageSource; @@ -244,6 +249,24 @@ public void proxiedListenersMixedWithTargetListeners() { assertThat(listener1.seenEvents).hasSize(2); } + /** + * Regression test for issue 28283, + * where event listeners proxied due to e.g. + * + * were added to the list of application listener beans twice (both proxy and unwrapped target). + */ + @Test + public void eventForSelfInjectedProxiedListenerFiredOnlyOnce() { + String basePackage = MyApplication.class.getPackageName(); + AbstractApplicationContext context = new AnnotationConfigApplicationContext(basePackage); + context.getBean(MyEventPublisher.class).publishMyEvent("hello"); + assertThat(MyEventListener.eventCount).isEqualTo(1); + context.close(); + } + @Test public void testEventPublicationInterceptor() throws Throwable { MethodInvocation invocation = mock(); diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyApplication.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyApplication.java new file mode 100644 index 000000000000..6686c1b121b8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyApplication.java @@ -0,0 +1,17 @@ +package org.springframework.context.event.test.self_inject; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.AbstractApplicationContext; + +@Configuration +@EnableAspectJAutoProxy(proxyTargetClass = true) +public class MyApplication { + public static void main(String[] args) { + try (AbstractApplicationContext context = new AnnotationConfigApplicationContext("org.springframework.context.event.test.self_inject")) { + context.getBean(MyEventPublisher.class).publishMyEvent("hello"); + assert MyEventListener.eventCount == 1 : "event listener must fire exactly once"; + } + } +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java new file mode 100644 index 000000000000..9b037195a9e6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java @@ -0,0 +1,15 @@ +package org.springframework.context.event.test.self_inject; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class MyAspect { + @Before("within(org.springframework.context.event.test.self_inject.MyEventListener)") + public void myAdvice(JoinPoint joinPoint) { + //System.out.println(joinPoint); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java new file mode 100644 index 000000000000..2dc0f2bccf72 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java @@ -0,0 +1,12 @@ +package org.springframework.context.event.test.self_inject; + +import org.springframework.context.ApplicationEvent; + +public class MyEvent extends ApplicationEvent { + private String message; + + public MyEvent(Object source, String message) { + super(source); + this.message = message; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java new file mode 100644 index 000000000000..5fddae77c05c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java @@ -0,0 +1,20 @@ +package org.springframework.context.event.test.self_inject; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class MyEventListener implements ApplicationListener { + public static int eventCount; + + @Autowired // use '-Dspring.main.allow-circular-references=true' in Spring Boot >= 2.6.0 + //@Lazy // with '@Lazy', the problem does not occur + private MyEventListener eventDemoListener; + + @Override + public void onApplicationEvent(MyEvent event) { + //System.out.println("Event: " + event); + eventCount++; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java new file mode 100644 index 000000000000..0959134f7c8b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java @@ -0,0 +1,15 @@ +package org.springframework.context.event.test.self_inject; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +public class MyEventPublisher { + @Autowired + private ApplicationEventPublisher eventPublisher; + + public void publishMyEvent(String message) { + eventPublisher.publishEvent(new MyEvent(this, message)); + } +}