diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index d408a7b6fd..020164115d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -237,33 +237,28 @@ public final class BeanInstanceSupplier extends AutowiredElementResolver impl private AutowiredArguments resolveArguments(RegisteredBean registeredBean, Executable executable) { Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, registeredBean.getBeanFactory()); - String beanName = registeredBean.getBeanName(); - Class beanClass = registeredBean.getBeanClass(); - AbstractAutowireCapableBeanFactory beanFactory = - (AbstractAutowireCapableBeanFactory) registeredBean.getBeanFactory(); - RootBeanDefinition mergedBeanDefinition = registeredBean.getMergedBeanDefinition(); + int startIndex = (executable instanceof Constructor constructor && ClassUtils.isInnerClass(constructor.getDeclaringClass())) ? 1 : 0; int parameterCount = executable.getParameterCount(); Object[] resolved = new Object[parameterCount - startIndex]; Assert.isTrue(this.shortcuts == null || this.shortcuts.length == resolved.length, () -> "'shortcuts' must contain " + resolved.length + " elements"); + + ConstructorArgumentValues argumentValues = resolveArgumentValues(registeredBean); Set autowiredBeans = new LinkedHashSet<>(resolved.length); - ConstructorArgumentValues argumentValues = resolveArgumentValues(beanFactory, - beanName, mergedBeanDefinition); for (int i = startIndex; i < parameterCount; i++) { MethodParameter parameter = getMethodParameter(executable, i); - DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(parameter, true); - String shortcut = (this.shortcuts != null) ? this.shortcuts[i - startIndex] : null; + DependencyDescriptor descriptor = new DependencyDescriptor(parameter, true); + String shortcut = (this.shortcuts != null ? this.shortcuts[i - startIndex] : null); if (shortcut != null) { - dependencyDescriptor = new ShortcutDependencyDescriptor( - dependencyDescriptor, shortcut, beanClass); + descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut, registeredBean.getBeanClass()); } ValueHolder argumentValue = argumentValues.getIndexedArgumentValue(i, null); - resolved[i - startIndex] = resolveArgument(registeredBean,autowiredBeans, - dependencyDescriptor, argumentValue); + resolved[i - startIndex] = resolveArgument(registeredBean, descriptor, argumentValue, autowiredBeans); } - registerDependentBeans(beanFactory, beanName, autowiredBeans); + registerDependentBeans(registeredBean.getBeanFactory(), registeredBean.getBeanName(), autowiredBeans); + return AutowiredArguments.of(resolved); } @@ -277,15 +272,14 @@ public final class BeanInstanceSupplier extends AutowiredElementResolver impl throw new IllegalStateException("Unsupported executable: " + executable.getClass().getName()); } - private ConstructorArgumentValues resolveArgumentValues( - AbstractAutowireCapableBeanFactory beanFactory, String beanName, - RootBeanDefinition mergedBeanDefinition) { - + private ConstructorArgumentValues resolveArgumentValues(RegisteredBean registeredBean) { ConstructorArgumentValues resolved = new ConstructorArgumentValues(); - if (mergedBeanDefinition.hasConstructorArgumentValues()) { + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + if (beanDefinition.hasConstructorArgumentValues() && + registeredBean.getBeanFactory() instanceof AbstractAutowireCapableBeanFactory beanFactory) { BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver( - beanFactory, beanName, mergedBeanDefinition, beanFactory.getTypeConverter()); - ConstructorArgumentValues values = mergedBeanDefinition.getConstructorArgumentValues(); + beanFactory, registeredBean.getBeanName(), beanDefinition, beanFactory.getTypeConverter()); + ConstructorArgumentValues values = beanDefinition.getConstructorArgumentValues(); values.getIndexedArgumentValues().forEach((index, valueHolder) -> { ValueHolder resolvedValue = resolveArgumentValue(valueResolver, valueHolder); resolved.addIndexedArgumentValue(index, resolvedValue); @@ -298,30 +292,27 @@ public final class BeanInstanceSupplier extends AutowiredElementResolver impl if (valueHolder.isConverted()) { return valueHolder; } - Object resolvedValue = resolver.resolveValueIfNecessary("constructor argument", - valueHolder.getValue()); - ValueHolder resolvedValueHolder = new ValueHolder(resolvedValue, - valueHolder.getType(), valueHolder.getName()); - resolvedValueHolder.setSource(valueHolder); - return resolvedValueHolder; + Object value = resolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue()); + ValueHolder resolvedHolder = new ValueHolder(value, valueHolder.getType(), valueHolder.getName()); + resolvedHolder.setSource(valueHolder); + return resolvedHolder; } @Nullable - private Object resolveArgument(RegisteredBean registeredBean, Set autowiredBeans, - DependencyDescriptor dependencyDescriptor, @Nullable ValueHolder argumentValue) { + private Object resolveArgument(RegisteredBean registeredBean, DependencyDescriptor descriptor, + @Nullable ValueHolder argumentValue, Set autowiredBeans) { TypeConverter typeConverter = registeredBean.getBeanFactory().getTypeConverter(); - Class parameterType = dependencyDescriptor.getMethodParameter().getParameterType(); if (argumentValue != null) { - return (!argumentValue.isConverted()) ? - typeConverter.convertIfNecessary(argumentValue.getValue(), parameterType) : - argumentValue.getConvertedValue(); + return (argumentValue.isConverted() ? argumentValue.getConvertedValue() : + typeConverter.convertIfNecessary(argumentValue.getValue(), + descriptor.getDependencyType(), descriptor.getMethodParameter())); } try { - return registeredBean.resolveAutowiredArgument(dependencyDescriptor, typeConverter, autowiredBeans); + return registeredBean.resolveAutowiredArgument(descriptor, typeConverter, autowiredBeans); } catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, registeredBean.getBeanName(), dependencyDescriptor, ex); + throw new UnsatisfiedDependencyException(null, registeredBean.getBeanName(), descriptor, ex); } } diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java index d591cf7421..5f3f0a6c95 100644 --- a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -41,19 +41,6 @@ public class PayloadApplicationEvent extends ApplicationEvent implements Reso private final ResolvableType payloadType; - /** - * Create a new PayloadApplicationEvent. - * @param source the object on which the event initially occurred (never {@code null}) - * @param payload the payload object (never {@code null}) - * @param payloadType the type object of payload object (can be {@code null}) - * @since 6.0 - */ - public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) { - super(source); - Assert.notNull(payload, "Payload must not be null"); - this.payload = payload; - this.payloadType = (payloadType != null) ? payloadType : ResolvableType.forInstance(payload); - } /** * Create a new PayloadApplicationEvent, using the instance to infer its type. @@ -64,6 +51,22 @@ public class PayloadApplicationEvent extends ApplicationEvent implements Reso this(source, payload, null); } + /** + * Create a new PayloadApplicationEvent based on the provided payload type. + * @param source the object on which the event initially occurred (never {@code null}) + * @param payload the payload object (never {@code null}) + * @param payloadType the type object of payload object (can be {@code null}). + * Note that this is meant to indicate the payload type (e.g. {@code String}), + * not the full event type (such as {@code PayloadApplicationEvent<<String>}). + * @since 6.0 + */ + public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) { + super(source); + Assert.notNull(payload, "Payload must not be null"); + this.payload = payload; + this.payloadType = (payloadType != null ? payloadType : ResolvableType.forInstance(payload)); + } + @Override public ResolvableType getResolvableType() { diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index c84e201d2f..daf6cd1e89 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -128,12 +128,12 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM @Override public void multicastEvent(ApplicationEvent event) { - multicastEvent(event, resolveDefaultEventType(event)); + multicastEvent(event, null); } @Override public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) { - ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event)); Executor executor = getTaskExecutor(); for (ApplicationListener listener : getApplicationListeners(event, type)) { if (executor != null) { @@ -145,10 +145,6 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM } } - private ResolvableType resolveDefaultEventType(ApplicationEvent event) { - return ResolvableType.forInstance(event); - } - /** * Invoke the given listener with the given event. * @param listener the ApplicationListener to invoke diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 347415b820..871d166e51 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -385,23 +385,47 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** * Publish the given event to all listeners. + *

This is the internal delegate that all other {@code publishEvent} + * methods refer to. It is not meant to be called directly but rather serves + * as a propagation mechanism between application contexts in a hierarchy, + * potentially overridden in subclasses for a custom propagation arrangement. * @param event the event to publish (may be an {@link ApplicationEvent} * or a payload object to be turned into a {@link PayloadApplicationEvent}) - * @param eventType the resolved event type, if known + * @param typeHint the resolved event type, if known. + * The implementation of this method also tolerates a payload type hint for + * a payload object to be turned into a {@link PayloadApplicationEvent}. + * However, the recommended way is to construct an actual event object via + * {@link PayloadApplicationEvent#PayloadApplicationEvent(Object, Object, ResolvableType)} + * instead for such scenarios. * @since 4.2 + * @see ApplicationEventMulticaster#multicastEvent(ApplicationEvent, ResolvableType) */ - protected void publishEvent(Object event, @Nullable ResolvableType eventType) { + protected void publishEvent(Object event, @Nullable ResolvableType typeHint) { Assert.notNull(event, "Event must not be null"); + ResolvableType eventType = null; // Decorate event as an ApplicationEvent if necessary ApplicationEvent applicationEvent; if (event instanceof ApplicationEvent applEvent) { applicationEvent = applEvent; + eventType = typeHint; } else { - applicationEvent = new PayloadApplicationEvent<>(this, event, eventType); - if (eventType == null) { - eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType(); + ResolvableType payloadType = null; + if (typeHint != null && ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) { + eventType = typeHint; + } + else { + payloadType = typeHint; + } + applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType); + } + + // Determine event type only once (for multicast and parent publish) + if (eventType == null) { + eventType = ResolvableType.forInstance(applicationEvent); + if (typeHint == null) { + typeHint = eventType; } } @@ -416,7 +440,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader // Publish event via parent context as well... if (this.parent != null) { if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) { - abstractApplicationContext.publishEvent(event, eventType); + abstractApplicationContext.publishEvent(event, typeHint); } else { this.parent.publishEvent(event); diff --git a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java index 2280dddb65..4bb408101b 100644 --- a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.util.List; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.PayloadApplicationEvent; @@ -68,16 +69,97 @@ class PayloadApplicationEventTests { }); } + @Test + @SuppressWarnings("resource") + void testEventClassWithPayloadType() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class); + + PayloadApplicationEvent> event = new PayloadApplicationEvent<>(this, + new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + ac.publishEvent(event); + assertThat(ac.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue(); + ac.close(); + } + + @Test + @SuppressWarnings("resource") + void testEventClassWithPayloadTypeOnParentContext() { + ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + PayloadApplicationEvent> event = new PayloadApplicationEvent<>(this, + new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + ac.publishEvent(event); + assertThat(parent.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue(); + ac.close(); + parent.close(); + } + + @Test + @SuppressWarnings("resource") + void testPayloadObjectWithPayloadType() { + final Object payload = new NumberHolder<>(42); + + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class) { + @Override + protected void finishRefresh() throws BeansException { + super.finishRefresh(); + // This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead + publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + } + }; + + assertThat(ac.getBean(NumberHolderListener.class).events.contains(payload)).isTrue(); + ac.close(); + } + + @Test + @SuppressWarnings("resource") + void testPayloadObjectWithPayloadTypeOnParentContext() { + final Object payload = new NumberHolder<>(42); + + ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent) { + @Override + protected void finishRefresh() throws BeansException { + super.finishRefresh(); + // This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead + publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + } + }; + ac.refresh(); + + assertThat(parent.getBean(NumberHolderListener.class).events.contains(payload)).isTrue(); + ac.close(); + parent.close(); + } + @Test @SuppressWarnings("resource") void testEventClassWithInterface() { ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class); + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); ac.publishEvent(event); assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue(); ac.close(); } + @Test + @SuppressWarnings("resource") + void testEventClassWithInterfaceOnParentContext() { + ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(AuditableListener.class); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); + ac.publishEvent(event); + assertThat(parent.getBean(AuditableListener.class).events.contains(event)).isTrue(); + ac.close(); + parent.close(); + } + @Test @SuppressWarnings("resource") void testProgrammaticEventListener() { @@ -96,6 +178,27 @@ class PayloadApplicationEventTests { ac.close(); } + @Test + @SuppressWarnings("resource") + void testProgrammaticEventListenerOnParentContext() { + List events = new ArrayList<>(); + ApplicationListener> listener = events::add; + ApplicationListener> mismatch = (event -> event.getPayload()); + + ConfigurableApplicationContext parent = new GenericApplicationContext(); + parent.addApplicationListener(listener); + parent.addApplicationListener(mismatch); + parent.refresh(); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); + ac.publishEvent(event); + assertThat(events.contains(event)).isTrue(); + ac.close(); + parent.close(); + } + @Test @SuppressWarnings("resource") void testProgrammaticPayloadListener() { @@ -108,12 +211,77 @@ class PayloadApplicationEventTests { ac.addApplicationListener(mismatch); ac.refresh(); - AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); - ac.publishEvent(event); - assertThat(events.contains(event.getPayload())).isTrue(); + String payload = "xyz"; + ac.publishEvent(payload); + assertThat(events.contains(payload)).isTrue(); ac.close(); } + @Test + @SuppressWarnings("resource") + void testProgrammaticPayloadListenerOnParentContext() { + List events = new ArrayList<>(); + ApplicationListener> listener = ApplicationListener.forPayload(events::add); + ApplicationListener> mismatch = ApplicationListener.forPayload(Integer::intValue); + + ConfigurableApplicationContext parent = new GenericApplicationContext(); + parent.addApplicationListener(listener); + parent.addApplicationListener(mismatch); + parent.refresh(); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + String payload = "xyz"; + ac.publishEvent(payload); + assertThat(events.contains(payload)).isTrue(); + ac.close(); + parent.close(); + } + + @Test + @SuppressWarnings("resource") + void testPlainPayloadListener() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PlainPayloadListener.class); + + String payload = "xyz"; + ac.publishEvent(payload); + assertThat(ac.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue(); + ac.close(); + } + + @Test + @SuppressWarnings("resource") + void testPlainPayloadListenerOnParentContext() { + ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(PlainPayloadListener.class); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + String payload = "xyz"; + ac.publishEvent(payload); + assertThat(parent.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue(); + ac.close(); + parent.close(); + } + + + static class NumberHolder { + + public NumberHolder(T number) { + } + } + + + @Component + public static class NumberHolderListener { + + public final List> events = new ArrayList<>(); + + @EventListener + public void onEvent(NumberHolder event) { + events.add(event); + } + } + public interface Auditable { } @@ -139,11 +307,16 @@ class PayloadApplicationEventTests { } } - static class NumberHolder { - public NumberHolder(T number) { + @Component + public static class PlainPayloadListener { + + public final List events = new ArrayList<>(); + + @EventListener + public void onEvent(String event) { + events.add(event); } - } } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java index f10034d916..033c8e50d9 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java @@ -57,8 +57,9 @@ public abstract class AbstractApplicationContextTests extends AbstractListableBe protected TestApplicationListener parentListener = new TestApplicationListener(); + @BeforeEach - public void setUp() throws Exception { + public void setup() throws Exception { this.applicationContext = createContext(); } @@ -79,6 +80,7 @@ public abstract class AbstractApplicationContextTests extends AbstractListableBe */ protected abstract ConfigurableApplicationContext createContext() throws Exception; + @Test public void contextAwareSingletonWasCalledBack() throws Exception { ACATester aca = (ACATester) applicationContext.getBean("aca");