Skip to content

Commit daf2911

Browse files
committed
Resolve ApplicationListener against BeanDefinition.getResolvableType()
This covers ApplicationListener generics in factory method return types in particular but also allows for programmatic setTargetType hints. Closes gh-23178
1 parent 8aa0b07 commit daf2911

File tree

5 files changed

+131
-20
lines changed

5 files changed

+131
-20
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.beans.BeanMetadataElement;
2020
import org.springframework.beans.MutablePropertyValues;
2121
import org.springframework.core.AttributeAccessor;
22+
import org.springframework.core.ResolvableType;
2223
import org.springframework.lang.Nullable;
2324

2425
/**
@@ -304,6 +305,17 @@ default boolean hasPropertyValues() {
304305

305306
// Read-only attributes
306307

308+
/**
309+
* Return a resolvable type for this bean definition,
310+
* based on the bean class or other specific metadata.
311+
* <p>This is typically fully resolved on a runtime-merged bean definition
312+
* but not necessarily on a configuration-time definition instance.
313+
* @return the resolvable type (potentially {@link ResolvableType#NONE})
314+
* @since 5.2
315+
* @see ConfigurableBeanFactory#getMergedBeanDefinition
316+
*/
317+
ResolvableType getResolvableType();
318+
307319
/**
308320
* Return whether this a <b>Singleton</b>, with a single, shared instance
309321
* returned on all calls.

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
3030
import org.springframework.beans.factory.config.BeanDefinition;
3131
import org.springframework.beans.factory.config.ConstructorArgumentValues;
32+
import org.springframework.core.ResolvableType;
3233
import org.springframework.core.io.DescriptiveResource;
3334
import org.springframework.core.io.Resource;
3435
import org.springframework.lang.Nullable;
@@ -458,6 +459,16 @@ public Class<?> resolveBeanClass(@Nullable ClassLoader classLoader) throws Class
458459
return resolvedClass;
459460
}
460461

462+
/**
463+
* Return a resolvable type for this bean definition.
464+
* <p>This implementation delegates to {@link #getBeanClass()}.
465+
* @since 5.2
466+
*/
467+
@Override
468+
public ResolvableType getResolvableType() {
469+
return (hasBeanClass() ? ResolvableType.forClass(getBeanClass()) : ResolvableType.NONE);
470+
}
471+
461472
/**
462473
* Set the name of the target scope for the bean.
463474
* <p>The default is singleton status, although this is only applied once

spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,14 +331,28 @@ public Class<?> getTargetType() {
331331
/**
332332
* Return a {@link ResolvableType} for this bean definition,
333333
* either from runtime-cached type information or from configuration-time
334-
* {@link #setTargetType(ResolvableType)} or {@link #setBeanClass(Class)}.
334+
* {@link #setTargetType(ResolvableType)} or {@link #setBeanClass(Class)},
335+
* also considering resolved factory method definitions.
335336
* @since 5.1
336-
* @see #getTargetType()
337-
* @see #getBeanClass()
337+
* @see #setTargetType(ResolvableType)
338+
* @see #setBeanClass(Class)
339+
* @see #setResolvedFactoryMethod(Method)
338340
*/
341+
@Override
339342
public ResolvableType getResolvableType() {
340343
ResolvableType targetType = this.targetType;
341-
return (targetType != null ? targetType : ResolvableType.forClass(getBeanClass()));
344+
if (targetType != null) {
345+
return targetType;
346+
}
347+
ResolvableType returnType = this.factoryMethodReturnType;
348+
if (returnType != null) {
349+
return returnType;
350+
}
351+
Method factoryMethod = this.factoryMethodToIntrospect;
352+
if (factoryMethod != null) {
353+
return ResolvableType.forMethodReturnType(factoryMethod);
354+
}
355+
return super.getResolvableType();
342356
}
343357

344358
/**

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

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.beans.factory.BeanFactory;
3030
import org.springframework.beans.factory.BeanFactoryAware;
3131
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
32+
import org.springframework.beans.factory.config.BeanDefinition;
3233
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
3334
import org.springframework.context.ApplicationEvent;
3435
import org.springframework.context.ApplicationListener;
@@ -70,7 +71,7 @@ public abstract class AbstractApplicationEventMulticaster
7071
private ClassLoader beanClassLoader;
7172

7273
@Nullable
73-
private BeanFactory beanFactory;
74+
private ConfigurableBeanFactory beanFactory;
7475

7576
private Object retrievalMutex = this.defaultRetriever;
7677

@@ -82,17 +83,17 @@ public void setBeanClassLoader(ClassLoader classLoader) {
8283

8384
@Override
8485
public void setBeanFactory(BeanFactory beanFactory) {
85-
this.beanFactory = beanFactory;
86-
if (beanFactory instanceof ConfigurableBeanFactory) {
87-
ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;
88-
if (this.beanClassLoader == null) {
89-
this.beanClassLoader = cbf.getBeanClassLoader();
90-
}
91-
this.retrievalMutex = cbf.getSingletonMutex();
86+
if (!(beanFactory instanceof ConfigurableBeanFactory)) {
87+
throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
88+
}
89+
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
90+
if (this.beanClassLoader == null) {
91+
this.beanClassLoader = this.beanFactory.getBeanClassLoader();
9292
}
93+
this.retrievalMutex = this.beanFactory.getSingletonMutex();
9394
}
9495

95-
private BeanFactory getBeanFactory() {
96+
private ConfigurableBeanFactory getBeanFactory() {
9697
if (this.beanFactory == null) {
9798
throw new IllegalStateException("ApplicationEventMulticaster cannot retrieve listener beans " +
9899
"because it is not associated with a BeanFactory");
@@ -221,6 +222,9 @@ private Collection<ApplicationListener<?>> retrieveApplicationListeners(
221222
listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
222223
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
223224
}
225+
226+
// Add programmatically registered listeners, including ones coming
227+
// from ApplicationListenerDetector (singleton beans and inner beans).
224228
for (ApplicationListener<?> listener : listeners) {
225229
if (supportsEvent(listener, eventType, sourceType)) {
226230
if (retriever != null) {
@@ -229,12 +233,14 @@ private Collection<ApplicationListener<?>> retrieveApplicationListeners(
229233
allListeners.add(listener);
230234
}
231235
}
236+
237+
// Add listeners by bean name, potentially overlapping with programmatically
238+
// registered listeners above - but here potentially with additional metadata.
232239
if (!listenerBeans.isEmpty()) {
233-
BeanFactory beanFactory = getBeanFactory();
240+
ConfigurableBeanFactory beanFactory = getBeanFactory();
234241
for (String listenerBeanName : listenerBeans) {
235242
try {
236-
Class<?> listenerType = beanFactory.getType(listenerBeanName);
237-
if (listenerType == null || supportsEvent(listenerType, eventType)) {
243+
if (supportsEvent(beanFactory, listenerBeanName, eventType)) {
238244
ApplicationListener<?> listener =
239245
beanFactory.getBean(listenerBeanName, ApplicationListener.class);
240246
if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
@@ -249,13 +255,24 @@ private Collection<ApplicationListener<?>> retrieveApplicationListeners(
249255
allListeners.add(listener);
250256
}
251257
}
258+
else {
259+
// Remove non-matching listeners that originally came from
260+
// ApplicationListenerDetector, possibly ruled out by additional
261+
// BeanDefinition metadata (e.g. factory method generics) above.
262+
Object listener = beanFactory.getSingleton(listenerBeanName);
263+
if (retriever != null) {
264+
retriever.applicationListeners.remove(listener);
265+
}
266+
allListeners.remove(listener);
267+
}
252268
}
253269
catch (NoSuchBeanDefinitionException ex) {
254270
// Singleton listener instance (without backing bean definition) disappeared -
255271
// probably in the middle of the destruction phase
256272
}
257273
}
258274
}
275+
259276
AnnotationAwareOrderComparator.sort(allListeners);
260277
if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
261278
retriever.applicationListeners.clear();
@@ -264,6 +281,42 @@ private Collection<ApplicationListener<?>> retrieveApplicationListeners(
264281
return allListeners;
265282
}
266283

284+
/**
285+
* Filter a bean-defined listener early through checking its generically declared
286+
* event type before trying to instantiate it.
287+
* <p>If this method returns {@code true} for a given listener as a first pass,
288+
* the listener instance will get retrieved and fully evaluated through a
289+
* {@link #supportsEvent(ApplicationListener, ResolvableType, Class)} call afterwards.
290+
* @param beanFactory the BeanFactory that contains the listener beans
291+
* @param listenerBeanName the name of the bean in the BeanFactory
292+
* @param eventType the event type to check
293+
* @return whether the given listener should be included in the candidates
294+
* for the given event type
295+
* @see #supportsEvent(Class, ResolvableType)
296+
* @see #supportsEvent(ApplicationListener, ResolvableType, Class)
297+
*/
298+
private boolean supportsEvent(
299+
ConfigurableBeanFactory beanFactory, String listenerBeanName, ResolvableType eventType) {
300+
301+
Class<?> listenerType = beanFactory.getType(listenerBeanName);
302+
if (listenerType == null || GenericApplicationListener.class.isAssignableFrom(listenerType) ||
303+
SmartApplicationListener.class.isAssignableFrom(listenerType)) {
304+
return true;
305+
}
306+
if (!supportsEvent(listenerType, eventType)) {
307+
return false;
308+
}
309+
try {
310+
BeanDefinition bd = beanFactory.getMergedBeanDefinition(listenerBeanName);
311+
ResolvableType genericEventType = bd.getResolvableType().as(ApplicationListener.class).getGeneric();
312+
return (genericEventType == ResolvableType.NONE || genericEventType.isAssignableFrom(eventType));
313+
}
314+
catch (NoSuchBeanDefinitionException ex) {
315+
// Ignore - no need to check resolvable type for manually registered singleton
316+
return true;
317+
}
318+
}
319+
267320
/**
268321
* Filter a listener early through checking its generically declared event
269322
* type before trying to instantiate it.
@@ -276,10 +329,6 @@ private Collection<ApplicationListener<?>> retrieveApplicationListeners(
276329
* for the given event type
277330
*/
278331
protected boolean supportsEvent(Class<?> listenerType, ResolvableType eventType) {
279-
if (GenericApplicationListener.class.isAssignableFrom(listenerType) ||
280-
SmartApplicationListener.class.isAssignableFrom(listenerType)) {
281-
return true;
282-
}
283332
ResolvableType declaredEventType = GenericApplicationListenerAdapter.resolveDeclaredEventType(listenerType);
284333
return (declaredEventType == null || declaredEventType.isAssignableFrom(eventType));
285334
}

spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.springframework.context.annotation.Configuration;
5353
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
5454
import org.springframework.context.annotation.Scope;
55+
import org.springframework.context.event.ContextClosedEvent;
5556
import org.springframework.context.event.ContextRefreshedEvent;
5657
import org.springframework.context.support.GenericApplicationContext;
5758
import org.springframework.tests.sample.beans.ITestBean;
@@ -273,6 +274,18 @@ public void configurationWithFunctionalRegistration() {
273274
assertThat(ctx.getBean(NestedTestBean.class).getCompany()).isEqualTo("functional");
274275
}
275276

277+
@Test
278+
public void configurationWithApplicationListener() {
279+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
280+
ctx.register(ConfigWithApplicationListener.class);
281+
ctx.refresh();
282+
ConfigWithApplicationListener config = ctx.getBean(ConfigWithApplicationListener.class);
283+
assertThat(config.closed).isFalse();
284+
ctx.close();
285+
assertThat(config.closed).isTrue();
286+
}
287+
288+
276289

277290
/**
278291
* Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
@@ -567,4 +580,16 @@ public NestedTestBean nestedTestBean(TestBean testBean) {
567580
}
568581
}
569582

583+
584+
@Configuration
585+
static class ConfigWithApplicationListener {
586+
587+
boolean closed = false;
588+
589+
@Bean
590+
public ApplicationListener<ContextClosedEvent> listener() {
591+
return (event -> this.closed = true);
592+
}
593+
}
594+
570595
}

0 commit comments

Comments
 (0)