When I use Spring ApplicationListener, in order to prevent transaction invalidation, my ApplicationListener implementation class writes the following code (of course, the code can be written differently to avoid this problem), which will cause my listener to trigger twice after the event is published. I think it's not normal, but not sure if it's a bug, so I want to ask everyone's opinion.
@Component
public class EventDemoListener implements ApplicationListener<EventDemo> {
@Autowired
DemoService1 demoService1;
@Autowired
DemoService2 demoService2;
@Autowired
EventDemoListener eventDemoListener;
@Override
public void onApplicationEvent(EventDemo event) {
eventDemoListener.testTransaction();
System.out.println("receiver " + event.getMessage());
}
@Transactional(rollbackFor = Exception.class)
public void testTransaction() {
demoService1.doService();
demoService2.doService();
}
}
Through this demo project, this problem can be reproduced. Please read the README.md document before running.
https://github.com/ZiFeng-Wu/spring-study
After analysis, because here DI itself , When EventDemoListener
is created, property filling will trigger DefaultSingletonBeanRegistry#getSingleton(String, boolean)
in advance.
Then singletonFactory.getObject()
executed in getSingleton()
will cause the unproxyed EventDemoListener
object to be put into AbstractAutoProxyCreator#earlyProxyReferences
.
After the properties are filled, calling AbstractAutowireCapableBeanFactory#initializeBean(String, Object, RootBeanDefinition)
and executing ApplicationListenerDetector#postProcessAfterInitialization(Object, String)
will cause the unproxyed EventDemoListener
object to be put into the AbstractApplicationEventMulticaster.DefaultListenerRetriever#applicationListeners
container.
Then when the event is published, execute AbstractApplicationEventMulticaster.DefaultListenerRetriever#getApplicationListeners()
and use ApplicationListener<?> listener =beanFactory.getBean(listenerBeanName, ApplicationListener.class)
to obtain the listener is the proxied EventDemoListener
object.
At this time, there are only unproxyed EventDemoListener
object in the applicationListeners
container, so the proxied EventDemoListener
object will be added to the final returned allListeners collection, as shown in the figure below, which will eventually cause the listener to be triggered twice.
Now with your updated GitHub project, I can reproduce the problem. It also occurs when using a Spring AOP aspect targeting the listener class, not just in the special case of self-injection + @Transactional
. IMO, it is a Spring Core bug, which is why I created PR #28322 in order to fix the issue #28283 you raised either before or after cross-posting here. You should have linked to that issue in your question, I just found it because I was searching for key words before creating an issue for it myself.
See also my comment in the issue, starting with this one.
OK, in your main class I changed
String configFile = "src/main/resources/spring-context.xml";
AbstractApplicationContext context = new FileSystemXmlApplicationContext(configFile);
to
AbstractApplicationContext context = new AnnotationConfigApplicationContext("com.zifeng.spring");
Now the application starts, also without DB configuration. It simply prints:
receiver test
There is no exception. I.e., if for you it does not work, probably there is a bug in your XML configuration. But actually, you really do not need it, because you used component and service annotations already.
So if I need a database setup in order to reproduce this, please, like I said in my comment, update the project to provide an H2 configuration which works out of the box.