문제

In some cases, we need to write to database in a Spring -application within an ApplicationListener, so we need transactions within the listener using @Transactional-annotation. These listeners are extended from an abstract baseclass, so normal ScopedProxyMode.INTERFACES won't do, as Spring container complains about expecting a bean of the abstract class-type, not "[$Proxy123]". However, using Scope(proxyMode=ScopedProxyMode.TARGET_CLASS), the listener receives the same event twice. We are using Spring version 3.1.3.RELEASE. (Edit: Still occurring with version 3.2.4.RELEASE)

Digging into Spring source with debugger, I found out that org.springframework.context.event.AbstractApplicationEventMulticaster.getApplicationListeners returns a LinkedList that contains the same listener twice (same instance: [com.example.TestEventListenerImpl@3aa6d0a4, com.example.TestEventListenerImpl@3aa6d0a4]), if the listener is a ScopedProxyMode.TARGET_CLASS.

Now, I can work around this by placing the code handling database write into a separate class and putting the @Transactional there, but my question is, is this a bug in Spring or expected behavior? Are there any other workarounds so we wouldn't need to create separate service-classes (ie. handle the transaction in the listener, but don't get the same event twice) for even the simplest cases?

Below is a smallish example showing the problem.

With @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS) in TestEventListenerImpl, the output is as follows:

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

With @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS) removed from TestEventListenerImpl, the output is:

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

So it seems that TARGET_CLASS -scoped beans get inserted twice into the listener list.

Example:

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"));
    }
}
도움이 되었습니까?

해결책

I can't speak to if this is a bug or expected behavior, but here's the dirty:

Declaring a bean like

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

Creates two BeanDefinition instances:

  1. A RootBeanDefinition describing the Scoped bean.
  2. A ScannedGenericBeanDefinition describing the actual object.

The ApplicationContext will use these bean definitions to create two beans:

  1. A ScopedProxyFactoryBean bean. This is a FactoryBean that wraps the TestEventListenerImpl object in a proxy.
  2. A TestEventListenerImpl bean. The actual TestEventListenerImpl object.

Part of the initialization process is to register beans that implement the ApplicationListener interface. The TestEventListenerImpl bean is created eagerly (right away) and registered as an ApplicationListener.

The ScopedProxyFactoryBean is lazy, the bean (proxy) it's supposed to create is only generated when requested. When that happens, it also gets registered as an ApplicationListener. You only see this when you explicitly request it

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

Or implicitly by using @Autowired to inject it into another bean. Note that the actual target object is added, not the proxy.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top