Skip to content
Advertisement

Using AOP and DI itself causes Spring ApplicationListener to be fired twice

Software versions

  • Spring Version 5.3.18 and earlier
  • JDK Version 1.8.0_202

Overview

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

Analysis

  1. After analysis, because here DI itself , When EventDemoListener is created, property filling will trigger DefaultSingletonBeanRegistry#getSingleton(String, boolean) in advance.

  2. Then singletonFactory.getObject() executed in getSingleton() will cause the unproxyed EventDemoListener object to be put into AbstractAutoProxyCreator#earlyProxyReferences.

  3. 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.

    enter image description here

  4. 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.

  5. 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.

    enter image description here

Advertisement

Answer

Updated answer

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.


Original answer (for reference)

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.

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement