使用 AOP 和 DI 本身会导致 Spring ApplicationListener 被触发两次

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

软件版本

概述

当我使用SpringApplicationListener时,为了防止事务失效,我的ApplicationListener实现class写了下面的代码(当然,代码可以写成不同的方式来避免这个问题),这将导致我的监听器在事件发布后触发两次。我觉得不正常,但不确定是不是bug,所以想问问大家的意见

@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();
    }
}

通过这个demo工程,可以复现这个问题。请在运行前阅读README.md文档。
https://github.com/ZiFeng-Wu/spring-study

分析

  1. 经过分析,因为这里DI本身,在创建EventDemoListener时,属性填充会提前触发DefaultSingletonBeanRegistry#getSingleton(String, boolean)

  2. 然后在getSingleton()中执行的singletonFactory.getObject()会导致未代理的EventDemoListener对象被放入AbstractAutoProxyCreator#earlyProxyReferences.

  3. 属性填充后,调用AbstractAutowireCapableBeanFactory#initializeBean(String, Object, RootBeanDefinition)并执行ApplicationListenerDetector#postProcessAfterInitialization(Object, String)会导致未代理的EventDemoListener对象放入AbstractApplicationEventMulticaster.DefaultListenerRetriever#applicationListeners容器.

  4. 然后事件发布的时候,执行AbstractApplicationEventMulticaster.DefaultListenerRetriever#getApplicationListeners(),使用ApplicationListener<?> listener =beanFactory.getBean(listenerBeanName, ApplicationListener.class)获取监听器就是被代理的EventDemoListener对象

  5. 此时applicationListeners容器中只有未代理的EventDemoListener对象,所以代理后的EventDemoListener对象会添加到最终返回的allListeners中集合,如下图所示,最终会导致监听器被触发两次。

更新答案

现在使用您更新的 GitHub 项目,我可以重现该问题。当使用 Spring AOP 方面以侦听器 class 为目标时,也会发生这种情况,而不仅仅是 self-injection + @Transactional 的特殊情况。 IMO,这是一个 Spring 核心错误,这就是为什么我在这里创建 PR #28322 in order to fix the issue #28283 你在 cross-posting 之前或之后提出的问题。你应该在你的问题中链接到那个问题,我刚刚找到它是因为我在为它自己创建问题之前搜索关键字。

另请参阅我在问题中的评论,从 this one 开始。


原回答(供参考)

好的,在你的主class我改了

String configFile = "src/main/resources/spring-context.xml";
AbstractApplicationContext context = new FileSystemXmlApplicationContext(configFile);

AbstractApplicationContext context = new AnnotationConfigApplicationContext("com.zifeng.spring");

现在应用程序启动了,同样没有数据库配置。它只是打印:

receiver test

也不例外。也就是说,如果它对您不起作用,可能是您的 XML 配置中存在错误。但实际上,你真的不需要它,因为你已经使用了组件和服务注解。

所以如果我需要一个数据库设置来重现这个,请像我在评论中说的那样更新项目以提供开箱即用的 H2 配置。