Skip to content

Commit e5ca4f7

Browse files
committed
GH-349 - Register parameters of methods annotated with @TransactionalEventListener for reflection.
1 parent 889c849 commit e5ca4f7

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

spring-modulith-events/spring-modulith-events-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
<scope>test</scope>
5353
</dependency>
5454

55+
<dependency>
56+
<groupId>org.springframework</groupId>
57+
<artifactId>spring-core-test</artifactId>
58+
<scope>test</scope>
59+
</dependency>
60+
5561
<dependency>
5662
<groupId>org.springframework</groupId>
5763
<artifactId>spring-aspects</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2023 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+
package org.springframework.modulith.events.aot;
17+
18+
import java.util.Arrays;
19+
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.aot.hint.MemberCategory;
23+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
24+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
25+
import org.springframework.beans.factory.support.RegisteredBean;
26+
import org.springframework.core.annotation.AnnotatedElementUtils;
27+
import org.springframework.transaction.event.TransactionalEventListener;
28+
29+
/**
30+
* A {@link BeanRegistrationAotProcessor} processing beans for methods annotated with {@link TransactionalEventListener}
31+
* to register those methods' parameter types for reflection as they will need to be serialized for the event
32+
* publication registry.
33+
*
34+
* @author Oliver Drotbohm
35+
* @since 1.1
36+
*/
37+
public class TransactionalEventListenerAotProcessor implements BeanRegistrationAotProcessor {
38+
39+
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalEventListenerAotProcessor.class);
40+
41+
/*
42+
* (non-Javadoc)
43+
* @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean)
44+
*/
45+
@Override
46+
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
47+
48+
Class<?> type = registeredBean.getBeanType().resolve(Object.class);
49+
50+
var methods = Arrays.stream(type.getDeclaredMethods())
51+
.filter(it -> AnnotatedElementUtils.hasAnnotation(it, TransactionalEventListener.class))
52+
.toList();
53+
54+
return methods.isEmpty() ? null : (context, __) -> {
55+
56+
var reflection = context.getRuntimeHints().reflection();
57+
58+
methods.forEach(method -> {
59+
60+
for (var it : method.getParameterTypes()) {
61+
62+
LOGGER.info("Registering {} (parameter of transactional event listener method {}) for reflection.",
63+
it.getSimpleName(), "%s.%s(…)".formatted(method.getDeclaringClass().getName(), method.getName()));
64+
65+
reflection.registerType(it,
66+
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
67+
}
68+
});
69+
};
70+
}
71+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
2+
org.springframework.modulith.events.aot.TransactionalEventListenerAotProcessor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2023 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+
package org.springframework.modulith.events.aot;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.aot.hint.MemberCategory;
22+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
23+
import org.springframework.aot.test.generate.TestGenerationContext;
24+
import org.springframework.beans.factory.aot.AotServices;
25+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
26+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
27+
import org.springframework.beans.factory.support.RegisteredBean;
28+
import org.springframework.beans.factory.support.RootBeanDefinition;
29+
import org.springframework.transaction.event.TransactionalEventListener;
30+
31+
/**
32+
* Integration tests for {@link TransactionalEventListenerAotProcessor}.
33+
*
34+
* @author Oliver Drotbohm
35+
*/
36+
class TransactionalEventListenerAotProcessorIntegrationTests {
37+
38+
@Test // GH-349
39+
void aotCustomizationsDiscoverable() {
40+
41+
assertThat(AotServices.factories().load(BeanRegistrationAotProcessor.class))
42+
.anyMatch(TransactionalEventListenerAotProcessor.class::isInstance);
43+
}
44+
45+
@Test // GH-349
46+
void registersEventListenerMethodParametersForReflection() {
47+
48+
var factory = new DefaultListableBeanFactory();
49+
factory.registerBeanDefinition("sample", new RootBeanDefinition(Sample.class));
50+
51+
var contribution = new TransactionalEventListenerAotProcessor()
52+
.processAheadOfTime(RegisteredBean.of(factory, "sample"));
53+
54+
assertThat(contribution).isNotNull();
55+
56+
var context = new TestGenerationContext();
57+
58+
contribution.applyTo(context, null);
59+
60+
assertThat(RuntimeHintsPredicates.reflection()
61+
.onType(MyEvent.class)
62+
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
63+
MemberCategory.INVOKE_DECLARED_METHODS)).accepts(context.getRuntimeHints());
64+
}
65+
66+
static class Sample {
67+
68+
@TransactionalEventListener
69+
void on(MyEvent event) {}
70+
}
71+
72+
record MyEvent(String payload) {}
73+
}

0 commit comments

Comments
 (0)