Skip to content

Commit bf3a478

Browse files
committed
Add support for functional registration of application listener
This commit adds a functional style registration of an application listener for a particular event. Rather than introducing another method at the ConfigurableApplicationContext interface level, this commit provides a factory method in GenericApplicationListener. Closes gh-21411
1 parent efb97cc commit bf3a478

File tree

4 files changed

+158
-1
lines changed

4 files changed

+158
-1
lines changed

spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.context.event;
1818

19+
import java.util.function.Consumer;
20+
1921
import org.springframework.context.ApplicationEvent;
2022
import org.springframework.context.ApplicationListener;
2123
import org.springframework.core.ResolvableType;
@@ -53,4 +55,16 @@ default boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
5355
*/
5456
boolean supportsEventType(ResolvableType eventType);
5557

58+
/**
59+
* Create a new {@code ApplicationListener} for the given event type.
60+
* @param eventType the event to listen to
61+
* @param consumer the consumer to invoke when a matching event is fired
62+
* @param <E> the specific {@code ApplicationEvent} subclass to listen to
63+
* @return a corresponding {@code ApplicationListener} instance
64+
* @since 6.1.3
65+
*/
66+
static <E extends ApplicationEvent> GenericApplicationListener forEventType(Class<E> eventType, Consumer<E> consumer) {
67+
return new GenericApplicationListenerDelegate<>(eventType, consumer);
68+
}
69+
5670
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.event;
18+
19+
import java.util.function.Consumer;
20+
21+
import org.springframework.context.ApplicationEvent;
22+
import org.springframework.core.ResolvableType;
23+
24+
/**
25+
* A {@link GenericApplicationListener} implementation that supports a single
26+
* event type.
27+
*
28+
* @author Stephane Nicoll
29+
* @since 6.1.3
30+
* @param <E> the specific {@code ApplicationEvent} subclass to listen to
31+
*/
32+
class GenericApplicationListenerDelegate<E extends ApplicationEvent> implements GenericApplicationListener {
33+
34+
private final Class<E> supportedEventType;
35+
36+
private final Consumer<E> consumer;
37+
38+
GenericApplicationListenerDelegate(Class<E> supportedEventType, Consumer<E> consumer) {
39+
this.supportedEventType = supportedEventType;
40+
this.consumer = consumer;
41+
}
42+
43+
@Override
44+
public void onApplicationEvent(ApplicationEvent event) {
45+
this.consumer.accept(this.supportedEventType.cast(event));
46+
}
47+
48+
@Override
49+
public boolean supportsEventType(ResolvableType eventType) {
50+
return this.supportedEventType.isAssignableFrom(eventType.toClass());
51+
}
52+
53+
}

spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import java.util.List;
2222
import java.util.Set;
2323
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.function.Consumer;
2425

2526
import org.aopalliance.intercept.MethodInvocation;
2627
import org.junit.jupiter.api.Test;
28+
import org.mockito.ArgumentCaptor;
2729

2830
import org.springframework.aop.framework.ProxyFactory;
2931
import org.springframework.beans.BeansException;
@@ -65,6 +67,7 @@
6567
import static org.mockito.Mockito.mock;
6668
import static org.mockito.Mockito.times;
6769
import static org.mockito.Mockito.verify;
70+
import static org.mockito.Mockito.verifyNoMoreInteractions;
6871
import static org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME;
6972

7073
/**
@@ -534,6 +537,20 @@ public void lambdaAsListenerWithJava9StyleClassCastMessage() {
534537
context.close();
535538
}
536539

540+
@Test
541+
@SuppressWarnings("unchecked")
542+
void addListenerWithConsumer() {
543+
Consumer<ContextRefreshedEvent> consumer = mock(Consumer.class);
544+
GenericApplicationContext context = new GenericApplicationContext();
545+
context.addApplicationListener(GenericApplicationListener.forEventType(
546+
ContextRefreshedEvent.class, consumer));
547+
context.refresh();
548+
ArgumentCaptor<ContextRefreshedEvent> captor = ArgumentCaptor.forClass(ContextRefreshedEvent.class);
549+
verify(consumer).accept(captor.capture());
550+
assertThat(captor.getValue().getApplicationContext()).isSameAs(context);
551+
verifyNoMoreInteractions(consumer);
552+
}
553+
537554
@Test
538555
public void beanPostProcessorPublishesEvents() {
539556
GenericApplicationContext context = new GenericApplicationContext();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.event;
18+
19+
import java.util.function.Consumer;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.InOrder;
23+
24+
import org.springframework.core.ResolvableType;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.mockito.Mockito.inOrder;
28+
import static org.mockito.Mockito.mock;
29+
30+
/**
31+
* Tests for {@link GenericApplicationListener}.
32+
*
33+
* @author Stephane Nicoll
34+
*/
35+
class GenericApplicationListenerTests extends AbstractApplicationEventListenerTests {
36+
37+
@Test
38+
void forEventTypeWithStrictTypeMatching() {
39+
GenericApplicationListener listener = GenericApplicationListener
40+
.forEventType(StringEvent.class, event -> {});
41+
assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue();
42+
}
43+
44+
@Test
45+
void forEventTypeWithSubClass() {
46+
GenericApplicationListener listener = GenericApplicationListener
47+
.forEventType(GenericTestEvent.class, event -> {});
48+
assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue();
49+
}
50+
51+
@Test
52+
void forEventTypeWithSuperClass() {
53+
GenericApplicationListener listener = GenericApplicationListener
54+
.forEventType(StringEvent.class, event -> {});
55+
assertThat(listener.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))).isFalse();
56+
}
57+
58+
@Test
59+
@SuppressWarnings("unchecked")
60+
void forEventTypeInvokesConsumer() {
61+
Consumer<StringEvent> consumer = mock(Consumer.class);
62+
GenericApplicationListener listener = GenericApplicationListener
63+
.forEventType(StringEvent.class, consumer);
64+
StringEvent event = new StringEvent(this, "one");
65+
StringEvent event2 = new StringEvent(this, "two");
66+
listener.onApplicationEvent(event);
67+
listener.onApplicationEvent(event2);
68+
InOrder ordered = inOrder(consumer);
69+
ordered.verify(consumer).accept(event);
70+
ordered.verify(consumer).accept(event2);
71+
}
72+
73+
}

0 commit comments

Comments
 (0)