Expose id/getListenerId in base EventListener/ApplicationListener (pulled up from tx)

Includes removeApplicationListeners(Predicate) method in ApplicationEventMulticaster.

Closes gh-26638
This commit is contained in:
Juergen Hoeller
2021-03-08 19:31:56 +01:00
parent 9877a9e6b7
commit 86902d27b2
10 changed files with 166 additions and 61 deletions

View File

@@ -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<E extends ApplicationEvent> extends EventLi
*/
void onApplicationEvent(E event);
/**
* Return an optional identifier for the listener.
* <p>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.

View File

@@ -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<ApplicationListener<?>> predicate) {
synchronized (this.defaultRetriever) {
this.defaultRetriever.applicationListeners.removeIf(predicate);
this.retrieverCache.clear();
}
}
@Override
public void removeApplicationListenerBeans(Predicate<String> predicate) {
synchronized (this.defaultRetriever) {
this.defaultRetriever.applicationListenerBeans.removeIf(predicate);
this.retrieverCache.clear();
}
}
@Override
public void removeAllListeners() {
synchronized (this.defaultRetriever) {

View File

@@ -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).
* <p>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<ApplicationListener<?>> 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).
* <p>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<String> predicate);
/**
* Remove all listeners registered with this multicaster.
* <p>After a remove call, the multicaster will perform no action
* on event notification until new listeners are registered.
* @see #removeApplicationListeners(Predicate)
*/
void removeAllListeners();

View File

@@ -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<ResolvableType> 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}.
* <p>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

View File

@@ -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 "";
}

View File

@@ -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 {

View File

@@ -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<E extends ApplicationEvent>
return Ordered.LOWEST_PRECEDENCE;
}
/**
* Return an identifier for the listener to be able to refer to it individually.
* <p>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.
* <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}.
@@ -67,17 +81,6 @@ public interface TransactionalApplicationListener<E extends ApplicationEvent>
return TransactionPhase.AFTER_COMMIT;
}
/**
* Return an identifier for the listener to be able to refer to it individually.
* <p>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.

View File

@@ -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<SynchronizationCallback> 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}.
* <p>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");

View File

@@ -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 {
* <p>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 "";
}

View File

@@ -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