【问题标题】:Duplicate event received on listener when using ScopedProxyMode.TARGET_CLASS使用 ScopedProxyMode.TARGET_CLASS 时在侦听器上收到重复事件
【发布时间】:2013-10-11 15:19:36
【问题描述】:

在某些情况下,我们需要在 ApplicationListener 内的 Spring 应用程序中写入数据库,因此我们需要在侦听器中使用 @Transactional-annotation 进行事务处理。这些侦听器是从抽象基类扩展而来的,所以普通的ScopedProxyMode.INTERFACES 不会这样做,因为 Spring 容器抱怨期待一个抽象类类型的 bean,而不是“[$Proxy123]”。但是,使用Scope(proxyMode=ScopedProxyMode.TARGET_CLASS),侦听器会收到两次相同的事件。我们使用的是 Spring 版本 3.1.3.RELEASE。 (编辑:版本 3.2.4.RELEASE 仍然存在)

使用调试器挖掘 Spring 源代码,我发现 org.springframework.context.event.AbstractApplicationEventMulticaster.getApplicationListeners 返回一个 LinkedList,其中包含两次相同的侦听器(相同的实例:[com.example.TestEventListenerImpl@3aa6d0a4, com.example.TestEventListenerImpl@3aa6d0a4] ),如果听众是ScopedProxyMode.TARGET_CLASS

现在,我可以通过将代码处理数据库写入一个单独的类并将@Transactional 放在那里来解决这个问题,但我的问题是,这是 Spring 中的错误还是预期的行为?是否有任何其他解决方法,因此即使是最简单的情况,我们也不需要创建单独的服务类(即在侦听器中处理事务,但不会两次获得相同的事件)?

下面是一个显示问题的小例子。

TestEventListenerImpl中带有@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS),输出如下:

Event com.example.TestEvent[source=Main] created by Main
Got event com.example.TestEvent[source=Main]
Got event com.example.TestEvent[source=Main]

从 TestEventListenerImpl 中删除 @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS),输出为:

Event com.example.TestEvent[source=Main] created by Main
Got event com.example.TestEvent[source=Main]

所以似乎 TARGET_CLASS 范围的 bean 被两次插入到侦听器列表中。

例子:

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="com.example/**"/>

</beans>

com.example.TestEvent

public class TestEvent extends ApplicationEvent
{
    public TestEvent(Object source)
    {
        super(source);
        System.out.println("Event " + this + " created by " + source);
    }
}

com.example.TestEventListener

public interface TestEventListener extends ApplicationListener<TestEvent>
{

    @Override
    public void onApplicationEvent(TestEvent event);

}

com.example.TestEventListenerImpl

@Component
@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)  //If commented out, the event won't be received twice
public class TestEventListenerImpl implements TestEventListener
{
    @Override
    public void onApplicationEvent(TestEvent event)
    {
        System.out.println("Got event " + event);
    }
}

com.example.ListenerTest

public class ListenerTest
{
    public static void main(String[] args)
    {
        ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

        SimpleApplicationEventMulticaster eventMulticaster = appContext.getBean(SimpleApplicationEventMulticaster.class);

        //This is also needed for the bug to reproduce
        TestEventListener listener = appContext.getBean(TestEventListener.class);

        eventMulticaster.multicastEvent(new TestEvent("Main"));
    }
}

【问题讨论】:

  • 是同一个Listener实例吗?顺便问下好问题!
  • 我运行了这个,我只得到一个事件。
  • 另外,您的示例代码中没有任何代理。
  • @SotiriosDelimanolis:您使用的是哪个 Spring 版本?至少这发生在使用 Maven 的 3.1.3.RELEASE 时。不记得CGlib版本,但我怀疑它会有所不同......我现在在家(在工作中写了这个问题),由于搬到一个新地方,目前只有一台非常旧的迷你笔记本电脑可以使用和翻新,所以现在不能用不同的版本测试它......希望他们已经在新版本中修复了它(如果它确实是一个 Spring 错误,就像我怀疑的那样)。
  • 我在3.2.4。也许重做你想展示给我们的例子,因为上面不会创建任何代理,它没有理由。

标签: spring listener cglib


【解决方案1】:

我无法确定这是错误还是预期的行为,但这是肮脏的:

声明一个类似的bean

@Component
@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)  //If commented out, the event won't be received twice
public class TestEventListenerImpl implements TestEventListener
{

创建两个BeanDefinition 实例:

  1. RootBeanDefinition 描述 Scoped bean。
  2. 描述实际对象的ScannedGenericBeanDefinition

ApplicationContext 将使用这些 bean 定义来创建两个 bean:

  1. ScopedProxyFactoryBean 豆。这是一个FactoryBean,它将TestEventListenerImpl 对象包装在一个代理中。
  2. TestEventListenerImpl 豆。实际的 TestEventListenerImpl 对象。

初始化过程的一部分是注册实现ApplicationListener 接口的bean。 TestEventListenerImpl bean 被急切地(立即)创建并注册为 ApplicationListener

ScopedProxyFactoryBean 是惰性的,它应该创建的 bean(代理)仅在请求时生成。发生这种情况时,它也会注册为ApplicationListener。只有在您明确请求时才会看到它

TestEventListener listener = appContext.getBean(TestEventListener.class);

或者通过使用@Autowired 将其隐式注入另一个bean。请注意,添加的是实际目标对象,而不是代理。

【讨论】:

  • 感谢您的洞察力!我将不得不检查我们的侦听器是否出于某种原因自动连接或从上下文中检索,看看我是否可以解决它,是否可以防止代理侦听器被调用两次。这真的不是什么大问题,但我宁愿让一个监听器在琐碎的情况下直接用@Transactional调用DAO接口(比如触发数据库记录更新的事件等),而不是整个监听器->服务接口-> 服务实现 -> DAO -dance,因为它似乎只是不必要的样板。
  • @esaj 我不太明白为什么你需要@Scope,如果它无论如何都是原型/单例。
  • 在实际用例中,侦听器派生自抽象基类,并且需要代理某些实现以使@Transactional-annotations 工作。如果没有代理,则不会创建任何事务。使用“正常”ScopedProxyMode.INTERFACES,应用程序容器无法启动,抱怨 AbstractApplicationEventMulticaster(或类似的)期望抽象类类型的侦听器 bean,而不是代理(如“[$Proxy123]”)。使用 TARGET_CLASS -proxies,一切似乎都正常,直到我们注意到事件在代理侦听器上触发了两次;P
  • @esaj 使用正确的 CGLIB 库 @Component@Transactional,即使没有 @Scope,您也应该获得 ENHANCED_BY_CGLIB 样式的代理。
  • 感谢您的提示,我一直在考虑诸如 AOP/加载时代码编织/whatsitcall-transactions 之类的东西,但还没有时间深入研究它。目前我想我们只需要在需要的地方使用监听器 -> 服务 -> DAO -route,因为还有很多其他的事情要做(像往常一样),但当我有机会时一定要看看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-04
  • 2018-01-25
  • 1970-01-01
  • 2017-08-30
  • 2017-07-16
  • 1970-01-01
相关资源
最近更新 更多