diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationListener.java b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java index 3194f52091..55254c5130 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.context; import java.util.EventListener; import java.util.function.Consumer; +import java.util.function.Predicate; /** * Interface to be implemented by application event listeners. @@ -46,6 +47,17 @@ public interface ApplicationListener extends EventLi */ void onApplicationEvent(E event); + /** + * Return an optional identifier for the listener. + *

The default value is an empty String. + * @since 5.3.5 + * @see org.springframework.context.event.EventListener#id + * @see org.springframework.context.event.ApplicationEventMulticaster#removeApplicationListeners(Predicate) + */ + default String getListenerId() { + return ""; + } + /** * Create a new {@code ApplicationListener} for the given payload consumer. diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index 9f0fd1d25b..d765539d0a 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -137,6 +138,22 @@ public abstract class AbstractApplicationEventMulticaster } } + @Override + public void removeApplicationListeners(Predicate> predicate) { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListeners.removeIf(predicate); + this.retrieverCache.clear(); + } + } + + @Override + public void removeApplicationListenerBeans(Predicate predicate) { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListenerBeans.removeIf(predicate); + this.retrieverCache.clear(); + } + } + @Override public void removeAllListeners() { synchronized (this.defaultRetriever) { diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java index 5810d8bcb3..9a23a1e9c0 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.context.event; +import java.util.function.Predicate; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; @@ -39,31 +41,68 @@ public interface ApplicationEventMulticaster { /** * Add a listener to be notified of all events. * @param listener the listener to add + * @see #removeApplicationListener(ApplicationListener) + * @see #removeApplicationListeners(Predicate) */ void addApplicationListener(ApplicationListener listener); /** * Add a listener bean to be notified of all events. * @param listenerBeanName the name of the listener bean to add + * @see #removeApplicationListenerBean(String) + * @see #removeApplicationListenerBeans(Predicate) */ void addApplicationListenerBean(String listenerBeanName); /** * Remove a listener from the notification list. * @param listener the listener to remove + * @see #addApplicationListener(ApplicationListener) + * @see #removeApplicationListeners(Predicate) */ void removeApplicationListener(ApplicationListener listener); /** * Remove a listener bean from the notification list. * @param listenerBeanName the name of the listener bean to remove + * @see #addApplicationListenerBean(String) + * @see #removeApplicationListenerBeans(Predicate) */ void removeApplicationListenerBean(String listenerBeanName); + /** + * Remove all matching listeners from the set of registered + * {@code ApplicationListener} instances (which includes adapter classes + * such as {@link ApplicationListenerMethodAdapter}, e.g. for annotated + * {@link EventListener} methods). + *

Note: This just applies to instance registrations, not to listeners + * registered by bean name. + * @param predicate the predicate to identify listener instances to remove, + * e.g. checking {@link ApplicationListener#getListenerId()} + * @since 5.3.5 + * @see #addApplicationListener(ApplicationListener) + * @see #removeApplicationListener(ApplicationListener) + */ + void removeApplicationListeners(Predicate> predicate); + + /** + * Remove all matching listener beans from the set of registered + * listener bean names (referring to bean classes which in turn + * implement the {@link ApplicationListener} interface directly). + *

Note: This just applies to bean name registrations, not to + * programmatically registered {@code ApplicationListener} instances. + * @param predicate the predicate to identify listener bean names to remove + * @since 5.3.5 + * @see #addApplicationListenerBean(String) + * @see #removeApplicationListenerBean(String) + */ + void removeApplicationListenerBeans(Predicate predicate); + /** * Remove all listeners registered with this multicaster. *

After a remove call, the multicaster will perform no action * on event notification until new listeners are registered. + * @see #removeApplicationListeners(Predicate) */ void removeAllListeners(); diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 02f4dc8093..e78c794ca5 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.StringJoiner; import java.util.concurrent.CompletionStage; import org.apache.commons.logging.Log; @@ -89,6 +90,9 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe private final int order; + @Nullable + private volatile String listenerId; + @Nullable private ApplicationContext applicationContext; @@ -113,6 +117,8 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); this.condition = (ann != null ? ann.condition() : null); this.order = resolveOrder(this.targetMethod); + String id = (ann != null ? ann.id() : ""); + this.listenerId = (!id.isEmpty() ? id : null); } private static List resolveDeclaredEventTypes(Method method, @Nullable EventListener ann) { @@ -186,6 +192,32 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe return this.order; } + @Override + public String getListenerId() { + String id = this.listenerId; + if (id == null) { + id = getDefaultListenerId(); + this.listenerId = id; + } + return id; + } + + /** + * Determine the default id for the target listener, to be applied in case of + * no {@link EventListener#id() annotation-specified id value}. + *

The default implementation builds a method name with parameter types. + * @since 5.3.5 + * @see #getListenerId() + */ + protected String getDefaultListenerId() { + Method method = getTargetMethod(); + StringJoiner sj = new StringJoiner(",", "(", ")"); + for (Class paramType : method.getParameterTypes()) { + sj.add(paramType.getName()); + } + return ClassUtils.getQualifiedMethodName(method) + sj.toString(); + } + /** * Process the specified {@link ApplicationEvent}, checking if the condition diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index 4d1930ef50..1de754d6d5 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.Predicate; import org.springframework.context.ApplicationEvent; import org.springframework.core.annotation.AliasFor; @@ -128,4 +129,13 @@ public @interface EventListener { */ String condition() default ""; + /** + * An optional identifier for the listener, defaulting to the fully-qualified + * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). + * @since 5.3.5 + * @see org.springframework.context.ApplicationListener#getListenerId() + * @see ApplicationEventMulticaster#removeApplicationListeners(Predicate) + */ + String id() default ""; + } diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index 95ead7c1d9..a5fe786e6e 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,6 +111,12 @@ public class AnnotationDrivenEventListenerTests { this.context.publishEvent(event); this.eventCollector.assertEvent(listener, event); this.eventCollector.assertTotalEventsCount(1); + + context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners( + l -> l.getListenerId().contains("TestEvent")); + this.eventCollector.clear(); + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(listener); } @Test @@ -126,6 +132,12 @@ public class AnnotationDrivenEventListenerTests { this.context.publishEvent(event); this.eventCollector.assertEvent(listener, event); this.eventCollector.assertTotalEventsCount(1); + + context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners( + l -> l.getListenerId().contains("TestEvent")); + this.eventCollector.clear(); + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(listener); } @Test @@ -138,6 +150,12 @@ public class AnnotationDrivenEventListenerTests { this.context.publishEvent(event); this.eventCollector.assertEvent(bean, event); this.eventCollector.assertTotalEventsCount(1); + + context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners( + l -> l.getListenerId().equals("foo")); + this.eventCollector.clear(); + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(bean); } @Test @@ -711,7 +729,7 @@ public class AnnotationDrivenEventListenerTests { } - @EventListener + @EventListener(id = "foo") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface FooListener { diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java index 49939b6dcd..c8c2fa5f7c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,6 +59,20 @@ public interface TransactionalApplicationListener return Ordered.LOWEST_PRECEDENCE; } + /** + * Return an identifier for the listener to be able to refer to it individually. + *

It might be necessary for specific completion callback implementations + * to provide a specific id, whereas for other scenarios an empty String + * (as the common default value) is acceptable as well. + * @see ApplicationListener#getListenerId() + * @see TransactionalEventListener#id + * @see #addCallback + */ + @Override + default String getListenerId() { + return ""; + } + /** * Return the {@link TransactionPhase} in which the listener will be invoked. *

The default phase is {@link TransactionPhase#AFTER_COMMIT}. @@ -67,17 +81,6 @@ public interface TransactionalApplicationListener return TransactionPhase.AFTER_COMMIT; } - /** - * Return an identifier for the listener to be able to refer to it individually. - *

It might be necessary for specific completion callback implementations - * to provide a specific id, whereas for other scenarios an empty String - * (as the common default value) is acceptable as well. - * @see #addCallback - */ - default String getListenerId() { - return ""; - } - /** * Add a callback to be invoked on processing within transaction synchronization, * i.e. when {@link #processEvent} is being triggered during actual transactions. diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index d2014fa18e..ce39e136c1 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +25,8 @@ import org.springframework.context.event.ApplicationListenerMethodAdapter; import org.springframework.context.event.EventListener; import org.springframework.context.event.GenericApplicationListener; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * {@link GenericApplicationListener} adapter that delegates the processing of @@ -55,9 +52,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi private final TransactionPhase transactionPhase; - @Nullable - private volatile String listenerId; - private final List callbacks = new CopyOnWriteArrayList<>(); @@ -84,31 +78,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi return this.transactionPhase; } - @Override - public String getListenerId() { - String id = this.listenerId; - if (id == null) { - id = this.annotation.id(); - if (id.isEmpty()) { - id = getDefaultListenerId(); - } - this.listenerId = id; - } - return id; - } - - /** - * Determine the default id for the target listener, to be applied in case of - * no {@link TransactionalEventListener#id() annotation-specified id value}. - *

The default implementation builds a method name with parameter types. - * @see #getListenerId() - */ - protected String getDefaultListenerId() { - Method method = getTargetMethod(); - return ClassUtils.getQualifiedMethodName(method) + - "(" + StringUtils.arrayToDelimitedString(method.getParameterTypes(), ",") + ")"; - } - @Override public void addCallback(SynchronizationCallback callback) { Assert.notNull(callback, "SynchronizationCallback must not be null"); diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java index 992f9e4c39..20968245c9 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,13 +64,6 @@ public @interface TransactionalEventListener { */ TransactionPhase phase() default TransactionPhase.AFTER_COMMIT; - /** - * An optional identifier to uniquely reference the listener. - * @since 5.3 - * @see TransactionalApplicationListener#getListenerId() - */ - String id() default ""; - /** * Whether the event should be processed if no transaction is running. */ @@ -98,6 +91,17 @@ public @interface TransactionalEventListener { *

The default is {@code ""}, meaning the event is always handled. * @see EventListener#condition */ + @AliasFor(annotation = EventListener.class, attribute = "condition") String condition() default ""; + /** + * An optional identifier for the listener, defaulting to the fully-qualified + * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). + * @since 5.3 + * @see EventListener#id + * @see TransactionalApplicationListener#getListenerId() + */ + @AliasFor(annotation = EventListener.class, attribute = "id") + String id() default ""; + } diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapterTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapterTests.java index f8bb2384c9..6c66837caf 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapterTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapterTests.java @@ -26,6 +26,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -81,7 +82,7 @@ public class TransactionalApplicationListenerMethodAdapterTests { assertThat(callback.postEvent).isEqualTo(event); assertThat(callback.ex).isNull(); assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.AFTER_COMMIT); - assertThat(adapter.getListenerId()).endsWith("SampleEvents.defaultPhase(class java.lang.String)"); + assertThat(adapter.getListenerId()).endsWith("SampleEvents.defaultPhase(java.lang.String)"); } @Test @@ -102,7 +103,7 @@ public class TransactionalApplicationListenerMethodAdapterTests { assertThat(callback.ex).isInstanceOf(RuntimeException.class); assertThat(callback.ex.getMessage()).isEqualTo("event"); assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.BEFORE_COMMIT); - assertThat(adapter.getListenerId()).isEqualTo(adapter.getDefaultListenerId()); + assertThat(adapter.getListenerId()).isEqualTo(ClassUtils.getQualifiedMethodName(m) + "(java.lang.String)"); } @Test