Spring Application Listeners
This review is focused on the basic concepts of Spring application listeners and has some tips on how to disable them when you need them.
Join the DZone community and get the full member experience.
Join For FreeI've recently faced a problem when disabling application listeners created using org.springframework.context.event.EventListener
annotation for org.springframework.context.event.ContextRefreshedEvent
event in unit tests. I found several decisions and decided to share them.
Quick Guide To Spring Application Listeners
First of all, here are some words about key objects that Spring uses when handling application events.
Application Event
Application events are implementations of org.springframework.context.ApplicationEvent
. Actually, Spring allows the use of any objects for publishing and handling. This is achieved by using org.springframework.context.PayloadApplicationEvent
. It extends org.springframework.context.ApplicationEvent
and is used as an adapter. Finally, we can say that application events are DTOs that keep specific information for each event.
Publisher
Publishers are implementations of org.springframework.context.ApplicationEventPublisher
interface. They publish events to listeners. In most cases, org.springframework.context.ApplicationContext
implementation is used as publisher. It delegates publishing to multicaster. Here is a code example,
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
abstractApplicationContext.publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
Multicaster
Multicasters are implementations of org.springframework.context.event.ApplicationEventMulticaster
interface. Its main purpose is to publish events to listeners as well as for publisher.
But, besides publisher, multicaster stores all application listeners, manages them, and determines which of them should fire for specific events.
By default, Spring uses org.springframework.context.event.SimpleApplicationEventMulticaster
.
Actually, Spring is looking for a bean named AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME
, if it exists in the application context, Spring uses this bean; otherwise, it creates an instance of SimpleApplicationEventMulticaster
and adds it as a singleton bean to beanFactory
.
Here is a code fragment.
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isTraceEnabled()) {
logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isTraceEnabled()) {
logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
}
}
}
All this stuff is done when executing ConfigurableApplicationContext#refresh()
method right before registering application listeners and after applying BeanFactoryPostProcessors
and registering messageSources
.
So, if you want to replace the default implementation of this bean, you have to create it before and use the same bean name. I will come back to it later.
ApplicationListener
I divided all listeners into two different groups because they created and registered in different ways. When I say 'register listener' I mean to add it to multicaster.
ApplicationListener Listener
'ApplicationListener listerners' are implementations of org.springframework.context.ApplicationListener
or org.springframework.context.event.SmartApplicationListener
interface. These listeners are beans and they are created as all other beans.
They can be ordered using annotation org.springframework.core.annotation.Order
or interfaces org.springframework.core.Ordered
and org.springframework.core.PriorityOrdered
and process events according to this order.
Such listeners are registered during ConfigurableApplicationContext#refresh()
execution right after initializing multicaster and before initializing org.springframework.beans.factory.SmartInitializingSingleton
s at the stage of finishing singleton initialization.
Be also aware that within this step, all early events are published. Early events are all events that are published before the multicaster is initialized in an application context.
Early events are handled only by 'ApplicationListener listerner'. Another type of application listener won't process such an event.
Keep it in mind if you want to disable such listeners. You will probably have to replace the default ApplicationEventMulticaster
bean for this purpose. There will be more details later.
@EventListener Listener
These are listeners created using org.springframework.context.event.EventListener
annotation. They are processed by org.springframework.context.event.EventListenerMethodProcessor
.
If the bean method contains this annotation, EventListenerMethodProcessor
creates instance of ApplicationListener<?>
or SmartApplicationListener
using org.springframework.context.event.EventListenerFactory.EventListenerFactory
based on annotation parameters and adds it to AbstractApplicationContext.applicationEventMulticaster
.
You can create as many EventListenerFactory
as you need and EventListenerMethodProcessor
will create the same amount of listeners for each method. Here is a code fragment from EventListenerMethodProcessor
.
for (Method method : annotatedMethods.keySet()) {
for (EventListenerFactory factory : factories) {
if (factory.supportsMethod(method)) {
Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));
ApplicationListener<?> applicationListener = factory.createApplicationListener(beanName, targetType, methodToUse);
if (applicationListener instanceof ApplicationListenerMethodAdapter) {
((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);
}
context.addApplicationListener(applicationListener);
break;
}
}
}
Spring itself has a default implementation org.springframework.context.event.DefaultEventListenerFactory
, which is enough in many cases. DefaultEventListenerFactory#createApplicationListener(String,Class<?>,Method)
creates org.springframework.context.event.ApplicationListenerMethodAdapter
instance, that implements SmartApplicationListener
.
The bean name for DefaultEventListenerFactory
is AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME
, so if you are going to replace this bean, take it into account.
As mentioned above all '@EventListener listener' are created and registered in EventListenerMethodProcessor
, to be more precise in EventListenerMethodProcessor#afterSingletonsInstantiated()
, implementation of org.springframework.beans.factory.SmartInitializingSingleton
. All SmartInitializingSingleton
s are initialized in ConfigurableListableBeanFactory#preInstantiateSingletons()
executed in ConfigurableApplicationContext#refresh()
.
ConfigurableListableBeanFactory#preInstantiateSingletons()
is executed almost at the end of application context refresh, right after registering listener beans, i.e. 'ApplicationListener listeners', and before initializing lifecycle beans.
So, listeners are registered before initializing lifecycle beans. Let's keep it in mind.
Now, let's take a look at different approaches to disabling application listeners.
Using org.springframework.context.event.EventListener@#condition
org.springframework.context.event.EventListener
has an attribute condition
which is the SPeL expression used for making the event handling conditional. The event will be handled if the expression evaluates to a boolean true
or one of the following strings: "true," "on," "yes," or "1".
It's possible to switch off listeners by application properties parameter, for example:
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
class TestListener {
@EventListener(condition = "@environment.getProperty('listeners.enabled')")
void handle(final ContextRefreshedEvent e) {
System.out.println("@EventListener works!");
}
}
I used a bean reference to org.springframework.core.env.Environment
, but not predefined environment['listeners.enabled']
, because EventListenerMethodProcessor
configures its own SPeL context
, which is an instance of org.springframework.context.expression.MethodBasedEvaluationContext
, and it does not contain required org.springframework.expression.PropertyAccessor
for org.springframework.core.env.Environment
like here:
public static void main(final String[] args) {
final var app = SpringApplication.run(Main.class, args);
final var expression = new SpelExpressionParser().parseExpression("environment['listeners.enabled']");
final var beanExpressionContext = new BeanExpressionContext(app.getBeanFactory(), null);
final var sec = new StandardEvaluationContext(beanExpressionContext);
sec.addPropertyAccessor(new BeanExpressionContextAccessor());
sec.addPropertyAccessor(new EnvironmentAccessor());
final var value = expression.getValue(sec, Boolean.class);
org.springframework.util.Assert.isTrue(value, "Incorrect 'listeners.enabled' value");
}
Using org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
This approach is also applicable to 'ApplicationListener listerners' as well. As I mentioned above, these listeners are beans and managed by Spring application context, at least created if you use prototype beans, so simply create beans marked with org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(name = "listeners.enabled", havingValue = "true")
class TestApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(final ContextRefreshedEvent event) {
System.out.println("ApplicationListener works");
}
}
You can do the same for '@EventListener listener's; however, if you have several methods marked with @EventListener
annotation, you will disable all of them.
Using org.springframework.context.SmartLifecycle Bean
As it was mentioned above multicaster and all listeners are registered before initializing lifecycle beans. As soon as lifecycle beans are initialized and started, the application context publishes org.springframework.context.event.ContextRefreshedEvent
event.
It means that all beans are created and initialized. All custom events are mostly published after context initialization, especially events for '@EventListener listeners,' so this approach is also applicable in many cases.
The idea is to register a bean that implements org.springframework.context.SmartLifecycle
interface and disables listeners when starting.
Here it is:
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SmartApplicationListener;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class DisableEventListenersBean implements SmartLifecycle {
private final Set<String> ids;
private final Set<String> names;
private final Set<Class<?>> types;
private final ApplicationEventMulticaster multicaster;
public DisableEventListenersBean(
final ApplicationEventMulticaster multicaster,
final String... ids
) {
this.multicaster = multicaster;
this.types = Collections.emptySet();
this.names = Collections.emptySet();
this.ids = Arrays.stream(ids).collect(Collectors.toSet());
}
public DisableEventListenersBean(
final ApplicationContext context,
final ApplicationEventMulticaster multicaster,
final Class<?>... types
) {
this(
Collections.emptySet(), Arrays.stream(types).collect(Collectors.toSet()), context, multicaster
);
}
public DisableEventListenersBean(
final Set<String> ids,
final Set<Class<?>> types,
final ApplicationContext context,
final ApplicationEventMulticaster multicaster
) {
this.multicaster = multicaster;
this.ids = defaultIfNull(ids, Collections.emptySet());
this.types = defaultIfNull(types, Collections.emptySet());
this.names = types.stream().map(context::getBeanNamesForType).flatMap(Arrays::stream).collect(Collectors.toSet());
}
@Override
public void start() {
multicaster.removeApplicationListeners(
listener -> {
final boolean toRemove;
if (listener instanceof SmartApplicationListener smart) {
toRemove = ids.contains(smart.getListenerId()) || shouldDisableForType(listener);
} else {
toRemove = shouldDisableForType(listener);
}
return toRemove;
}
);
multicaster.removeApplicationListenerBeans(
names::contains
);
}
private boolean shouldDisableForType(final ApplicationListener<?> type) {
final var actual = AopProxyUtils.ultimateTargetClass(type);
return types.contains(actual);
}
@Override
public void stop() {
}
@Override
public boolean isRunning() {
return false;
}
}
When discussing 'ApplicationListener listerner' I mentioned org.springframework.context.event.SmartApplicationListener
interface.
It has additional methods, one of which is org.springframework.context.event.SmartApplicationListener#getListenerId()
.
Moreover, org.springframework.context.event.EventListener
annotation has the corresponding attribute org.springframework.context.event.EventListener#id
and it has the default value constructed like "mypackage.MyClass.myMethod()"
.
As discussed above '@EventListener listeners' are registered using EventListenerFactories
. DefaultEventListenerFactory
creates ApplicationListenerMethodAdapter
instances and add them to multicaster, so they all have the same type and org.springframework.context.event.SmartApplicationListener#getListenerId()
is the only parameter that helps to identify them.
That's why I used it in my code.
However, org.springframework.context.event.SmartApplicationListener#getListenerId()
is optional and might be an empty string, so it will not be superfluous to check types as well.
Besides, when removing 'ApplicationListener listerner' we have to remove them from both applicationListeners
and applicationListenerBeans
at least for my Spring version 6.04 (Spring Boot version 3.02).
I also added SmartApplicationListener
implementation in addition to what was already provided above TestApplicationListener
and TestListener
.
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;
@Component
class TestSmartApplicationListener implements SmartApplicationListener {
@Override
public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {
return ContextRefreshedEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(final ApplicationEvent event) {
System.out.println("SmartApplicationListener works");
}
}
Ensure listeners.enabled = true
in your application.properties
in order to be sure that TestApplicationListener
and TestListener
are not disabled by application properties parameter.
Here are several examples of the configuration of DisableEventListenersBean
bean.
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import java.util.Set;
@Configuration
class CustomApplicationEventConfig {
// @Bean
// SmartLifecycle disableEventListenersBean(final ApplicationEventMulticaster multicaster) {
// return new DisableEventListenersBean(
// multicaster,
// "com.example.demo.events.app.TestListener.handle(org.springframework.context.event.ContextRefreshedEvent)"
// );
// }
// @Bean
// SmartLifecycle disableEventListenersBean(
// final ApplicationContext context, final ApplicationEventMulticaster multicaster
// ) {
// return new DisableEventListenersBean(
// context,
// multicaster,
// TestApplicationListener.class
// );
// }
@Bean
SmartLifecycle disableEventListenersBean(
final ApplicationContext context, final ApplicationEventMulticaster multicaster
) {
return new DisableEventListenersBean(
Set.of(
"com.example.demo.events.app.TestListener.handle(org.springframework.context.event.ContextRefreshedEvent)"
),
Set.of(
TestApplicationListener.class, TestSmartApplicationListener.class
),
context,
multicaster
);
}
}
The last example disables all my application listeners.
Replacing Default org.springframework.context.event.EventListenerFactory bean
If the previous implementation is not enough to disable '@EventListener listeners,' let's replace the default org.springframework.context.event.EventListenerFactory
bean in the application context to not create any instances of SmartApplicationListener
for the specific annotated methods.
I simplified factory implementation, for example, purpose, to process only types. I think if you need to disable only some annotated methods within the same class, you can easily add method information, i.e., method name and parameter type.
So, here is the factory:
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationListenerMethodAdapter;
import org.springframework.context.event.EventListenerFactory;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class DisableEventListenerFactory implements EventListenerFactory {
private final Set<Class<?>> types;
public DisableEventListenerFactory(final Class<?>... types) {
this.types = Arrays.stream(types).collect(Collectors.toSet());
}
@Override
public boolean supportsMethod(final Method method) {
return true;
}
@Override
public ApplicationListener<?> createApplicationListener(
final String name, final Class<?> type, final Method method
) {
final ApplicationListener<?> listener;
if (types.contains(ClassUtils.getUserClass(type))) {
listener = event -> {};
} else {
listener = new ApplicationListenerMethodAdapter(name, type, method);
}
return listener;
}
}
Now, let's register the bean. Here is the implementation of org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
import lombok.SneakyThrows;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.stereotype.Component;
@Component
class RegisterEventListenerFactoryProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) throws BeansException {
if (registry.containsBeanDefinition(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)) {
registry.removeBeanDefinition(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME);
}
registry.registerBeanDefinition(
AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME,
createDisableEventListenerFactoryBeanDefinition()
);
}
@Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@SneakyThrows
private RootBeanDefinition createDisableEventListenerFactoryBeanDefinition() {
final var bd = new RootBeanDefinition();
bd.setScope(BeanDefinition.SCOPE_SINGLETON);
bd.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
bd.setBeanClass(DisableEventListenerFactory.class);
final var constructorValues = new ConstructorArgumentValues();
constructorValues.addIndexedArgumentValue(
0,
new Class<?>[] {
Class.forName("com.example.demo.events.app.TestListener")
}
);
bd.setConstructorArgumentValues(constructorValues);
return bd;
}
}
Default bean definition created in org.springframework.context.annotation.AnnotationConfigUtils
.
if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
}
Spring checks if the bean definition already exists and creates its own otherwise. As soon as Spring adds bean definition, we can't create beans like we used to because there will be two bean definitions and setting spring.main.allow-bean-definition-overriding=true
will not give any guarantees that our implementation will be used.
I mean that such an approach:
import lombok.SneakyThrows;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListenerFactory;
@Configuration
class CustomApplicationEventConfig {
@SneakyThrows
@Bean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)
EventListenerFactory disableEventListenerFactory() {
return new DisableEventListenerFactory(
Class.forName("com.example.demo.events.app.TestListener")
);
}
}
will lead to error The bean 'org.springframework.context.event.internalEventListenerFactory', defined in class path resource [com/example/demo/events/app/CustomApplicationEventConfig.class], could not be registered. A bean with that name has already been defined and overriding is disabled.
You can switch on overriding by setting spring.main.allow-bean-definition-overriding=true
in application.properties
, but be careful.
org.springframework.context.ApplicationContextInitializer
can also be used.
import lombok.SneakyThrows;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
public class DisableEventListenerApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
@SneakyThrows
public void initialize(final ConfigurableApplicationContext context) {
context.getBeanFactory()
.registerSingleton(
AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME,
new DisableEventListenerFactory(
Class.forName("com.example.demo.events.app.TestListener")
)
);
}
}
Add META-INF/spring.factories
to create initializer
org.springframework.context.ApplicationContextInitializer = com.example.demo.events.app.DisableEventListenerApplicationContextInitializer
For test purposes, you can use org.springframework.test.context.ContextConfiguration
as well:
@ContextConfiguration(initializers = DisableEventListenerApplicationContextInitializer.class)
I used java.lang.Class.forName(String)
everywhere because com.example.demo.events.app.TestListener
has package-private access.
Replacing Default org.springframework.context.event.ApplicationEventMulticaster bean
To disable 'ApplicationListener listerners', but not '@EventListener listeners', in cases when approach described in 'Using org.springframework.context.SmartLifecycle bean' is not enough; default org.springframework.context.event.ApplicationEventMulticaster
can be replaced.
As it was mentioned above, this bean is created during ConfigurableApplicationContext#refresh()
. Spring first checks it is already in context and creates its own instance otherwise.
Here is multicaster itself.
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class DisableApplicationEventMulticaster extends SimpleApplicationEventMulticaster {
private final Set<Class<ApplicationListener<?>>> types;
public DisableApplicationEventMulticaster(final Class<ApplicationListener<?>>... types) {
this.types = Arrays.stream(types).collect(Collectors.toSet());
}
public DisableApplicationEventMulticaster(final BeanFactory factory, final Class<ApplicationListener<?>>... types) {
super(factory);
this.types = Arrays.stream(types).collect(Collectors.toSet());
}
@Override
protected void invokeListener(final ApplicationListener<?> listener, final ApplicationEvent event) {
if (!types.contains(AopProxyUtils.ultimateTargetClass(listener))) {
super.invokeListener(listener, event);
}
}
}
It simply does not execute the listener`s methods when it is required.
Finally, create a bean.
@Configuration
class CustomApplicationEventConfig {
@SneakyThrows
@Bean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
ApplicationEventMulticaster disableApplicationEventMulticaster() {
return new DisableApplicationEventMulticaster(
(Class<ApplicationListener<?>>) Class.forName("com.example.demo.events.app.TestApplicationListener"),
(Class<ApplicationListener<?>>) Class.forName("com.example.demo.events.app.TestSmartApplicationListener")
);
}
}
DisableApplicationEventMulticaster
will disable all 'ApplicationListener listerners' which types are passed in its constructor, but not '@EventListener listeners'. For '@EventListener listeners' use another approach.
Conclusion
I used Spring Boot version 3.0.2 (Spring version 6.0.4) and Java 17 for the examples described above.
So, enjoy your application listeners and disable them when you need them!
Opinions expressed by DZone contributors are their own.
Comments