From e5ca4f7183d57cfc560375c132409c230662d9e1 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 27 Oct 2023 13:45:25 +0200 Subject: [PATCH] GH-349 - Register parameters of methods annotated with @TransactionalEventListener for reflection. --- .../spring-modulith-events-core/pom.xml | 6 ++ ...ransactionalEventListenerAotProcessor.java | 71 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 2 + ...tListenerAotProcessorIntegrationTests.java | 73 +++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessor.java create mode 100644 spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessorIntegrationTests.java diff --git a/spring-modulith-events/spring-modulith-events-core/pom.xml b/spring-modulith-events/spring-modulith-events-core/pom.xml index d4fadacb..efcdf614 100644 --- a/spring-modulith-events/spring-modulith-events-core/pom.xml +++ b/spring-modulith-events/spring-modulith-events-core/pom.xml @@ -52,6 +52,12 @@ test + + org.springframework + spring-core-test + test + + org.springframework spring-aspects diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessor.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessor.java new file mode 100644 index 00000000..3ff9026f --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessor.java @@ -0,0 +1,71 @@ +/* + * Copyright 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. + * 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.modulith.events.aot; + +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * A {@link BeanRegistrationAotProcessor} processing beans for methods annotated with {@link TransactionalEventListener} + * to register those methods' parameter types for reflection as they will need to be serialized for the event + * publication registry. + * + * @author Oliver Drotbohm + * @since 1.1 + */ +public class TransactionalEventListenerAotProcessor implements BeanRegistrationAotProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalEventListenerAotProcessor.class); + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean) + */ + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + + Class type = registeredBean.getBeanType().resolve(Object.class); + + var methods = Arrays.stream(type.getDeclaredMethods()) + .filter(it -> AnnotatedElementUtils.hasAnnotation(it, TransactionalEventListener.class)) + .toList(); + + return methods.isEmpty() ? null : (context, __) -> { + + var reflection = context.getRuntimeHints().reflection(); + + methods.forEach(method -> { + + for (var it : method.getParameterTypes()) { + + LOGGER.info("Registering {} (parameter of transactional event listener method {}) for reflection.", + it.getSimpleName(), "%s.%s(…)".formatted(method.getDeclaringClass().getName(), method.getName())); + + reflection.registerType(it, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + }); + }; + } +} diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring/aot.factories b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000..6cd19845 --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ + org.springframework.modulith.events.aot.TransactionalEventListenerAotProcessor diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessorIntegrationTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessorIntegrationTests.java new file mode 100644 index 00000000..5433f5ca --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/aot/TransactionalEventListenerAotProcessorIntegrationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 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. + * 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.modulith.events.aot; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * Integration tests for {@link TransactionalEventListenerAotProcessor}. + * + * @author Oliver Drotbohm + */ +class TransactionalEventListenerAotProcessorIntegrationTests { + + @Test // GH-349 + void aotCustomizationsDiscoverable() { + + assertThat(AotServices.factories().load(BeanRegistrationAotProcessor.class)) + .anyMatch(TransactionalEventListenerAotProcessor.class::isInstance); + } + + @Test // GH-349 + void registersEventListenerMethodParametersForReflection() { + + var factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("sample", new RootBeanDefinition(Sample.class)); + + var contribution = new TransactionalEventListenerAotProcessor() + .processAheadOfTime(RegisteredBean.of(factory, "sample")); + + assertThat(contribution).isNotNull(); + + var context = new TestGenerationContext(); + + contribution.applyTo(context, null); + + assertThat(RuntimeHintsPredicates.reflection() + .onType(MyEvent.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS)).accepts(context.getRuntimeHints()); + } + + static class Sample { + + @TransactionalEventListener + void on(MyEvent event) {} + } + + record MyEvent(String payload) {} +}