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:
RootBeanDefinition
describing the Scoped bean. ScannedGenericBeanDefinition
describing the actual object.The ApplicationContext
will use these bean definitions to create two beans:
ScopedProxyFactoryBean
bean. This is a FactoryBean
that wraps the TestEventListenerImpl
object in a proxy. 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.