diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java index 763f96f533..185b627b75 100644 --- a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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.Consumer; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; @@ -53,4 +55,16 @@ public interface GenericApplicationListener extends SmartApplicationListener { */ boolean supportsEventType(ResolvableType eventType); + /** + * Create a new {@code ApplicationListener} for the given event type. + * @param eventType the event to listen to + * @param consumer the consumer to invoke when a matching event is fired + * @param the specific {@code ApplicationEvent} subclass to listen to + * @return a corresponding {@code ApplicationListener} instance + * @since 6.1.3 + */ + static GenericApplicationListener forEventType(Class eventType, Consumer consumer) { + return new GenericApplicationListenerDelegate<>(eventType, consumer); + } + } diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java new file mode 100644 index 0000000000..f152b86c5a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.util.function.Consumer; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.ResolvableType; + +/** + * A {@link GenericApplicationListener} implementation that supports a single + * event type. + * + * @author Stephane Nicoll + * @since 6.1.3 + * @param the specific {@code ApplicationEvent} subclass to listen to + */ +class GenericApplicationListenerDelegate implements GenericApplicationListener { + + private final Class supportedEventType; + + private final Consumer consumer; + + GenericApplicationListenerDelegate(Class supportedEventType, Consumer consumer) { + this.supportedEventType = supportedEventType; + this.consumer = consumer; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.consumer.accept(this.supportedEventType.cast(event)); + } + + @Override + public boolean supportsEventType(ResolvableType eventType) { + return this.supportedEventType.isAssignableFrom(eventType.toClass()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index 9088442dfa..19b44968fb 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -21,9 +21,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.BeansException; @@ -65,6 +67,7 @@ import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME; /** @@ -534,6 +537,20 @@ public class ApplicationContextEventTests extends AbstractApplicationEventListen context.close(); } + @Test + @SuppressWarnings("unchecked") + void addListenerWithConsumer() { + Consumer consumer = mock(Consumer.class); + GenericApplicationContext context = new GenericApplicationContext(); + context.addApplicationListener(GenericApplicationListener.forEventType( + ContextRefreshedEvent.class, consumer)); + context.refresh(); + ArgumentCaptor captor = ArgumentCaptor.forClass(ContextRefreshedEvent.class); + verify(consumer).accept(captor.capture()); + assertThat(captor.getValue().getApplicationContext()).isSameAs(context); + verifyNoMoreInteractions(consumer); + } + @Test public void beanPostProcessorPublishesEvents() { GenericApplicationContext context = new GenericApplicationContext(); diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java new file mode 100644 index 0000000000..d1d23a2d45 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GenericApplicationListener}. + * + * @author Stephane Nicoll + */ +class GenericApplicationListenerTests extends AbstractApplicationEventListenerTests { + + @Test + void forEventTypeWithStrictTypeMatching() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue(); + } + + @Test + void forEventTypeWithSubClass() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(GenericTestEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue(); + } + + @Test + void forEventTypeWithSuperClass() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void forEventTypeInvokesConsumer() { + Consumer consumer = mock(Consumer.class); + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, consumer); + StringEvent event = new StringEvent(this, "one"); + StringEvent event2 = new StringEvent(this, "two"); + listener.onApplicationEvent(event); + listener.onApplicationEvent(event2); + InOrder ordered = inOrder(consumer); + ordered.verify(consumer).accept(event); + ordered.verify(consumer).accept(event2); + } + +}