Skip to content

Commit 4cca190

Browse files
committed
Add support of declarative use of reflection
This commit adds a `@Reflective` annotation that can be used to declare that the annotated element requires reflection at runtime. By default, the annotated element is exposed but this can be customized by specifying a dedicated `ReflectiveProcessor`. Closes gh-28469
1 parent 2517c72 commit 4cca190

File tree

8 files changed

+684
-0
lines changed

8 files changed

+684
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.context.aot;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.lang.reflect.Constructor;
21+
import java.util.Arrays;
22+
import java.util.HashMap;
23+
import java.util.LinkedHashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.Consumer;
28+
29+
import org.springframework.aot.generate.GenerationContext;
30+
import org.springframework.aot.hint.MemberCategory;
31+
import org.springframework.aot.hint.ReflectionHints;
32+
import org.springframework.aot.hint.RuntimeHints;
33+
import org.springframework.aot.hint.TypeHint.Builder;
34+
import org.springframework.aot.hint.annotation.Reflective;
35+
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
36+
import org.springframework.aot.hint.support.RuntimeHintsUtils;
37+
import org.springframework.beans.BeanUtils;
38+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
39+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
40+
import org.springframework.beans.factory.aot.BeanRegistrationCode;
41+
import org.springframework.beans.factory.support.RegisteredBean;
42+
import org.springframework.core.annotation.MergedAnnotation;
43+
import org.springframework.core.annotation.MergedAnnotations;
44+
import org.springframework.lang.Nullable;
45+
import org.springframework.util.ReflectionUtils;
46+
47+
/**
48+
* AOT {@code BeanRegistrationAotProcessor} that detects the presence of
49+
* {@link Reflective @Reflective} on annotated elements and invoke the
50+
* underlying {@link ReflectiveProcessor} implementations.
51+
*
52+
* @author Stephane Nicoll
53+
*/
54+
class ReflectiveProcessorBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
55+
56+
private final Map<Class<? extends ReflectiveProcessor>, ReflectiveProcessor> processors = new HashMap<>();
57+
58+
@Nullable
59+
@Override
60+
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
61+
Class<?> beanClass = registeredBean.getBeanClass();
62+
Set<Entry> entries = new LinkedHashSet<>();
63+
if (isReflective(beanClass)) {
64+
entries.add(createEntry(beanClass));
65+
}
66+
doWithReflectiveConstructors(beanClass, constructor ->
67+
entries.add(createEntry(constructor)));
68+
ReflectionUtils.doWithFields(beanClass, field ->
69+
entries.add(createEntry(field)), this::isReflective);
70+
ReflectionUtils.doWithMethods(beanClass, method ->
71+
entries.add(createEntry(method)), this::isReflective);
72+
if (!entries.isEmpty()) {
73+
return new ReflectiveProcessorBeanRegistrationAotContribution(entries);
74+
}
75+
return null;
76+
}
77+
78+
private void doWithReflectiveConstructors(Class<?> beanClass, Consumer<Constructor<?>> consumer) {
79+
for (Constructor<?> constructor : beanClass.getDeclaredConstructors()) {
80+
if (isReflective(constructor)) {
81+
consumer.accept(constructor);
82+
}
83+
}
84+
}
85+
86+
private boolean isReflective(AnnotatedElement element) {
87+
return MergedAnnotations.from(element).isPresent(Reflective.class);
88+
}
89+
90+
@SuppressWarnings("unchecked")
91+
private Entry createEntry(AnnotatedElement element) {
92+
Class<? extends ReflectiveProcessor>[] processorClasses = (Class<? extends ReflectiveProcessor>[])
93+
MergedAnnotations.from(element).get(Reflective.class).getClassArray("value");
94+
List<ReflectiveProcessor> processors = Arrays.stream(processorClasses).distinct()
95+
.map(processorClass -> this.processors.computeIfAbsent(processorClass, BeanUtils::instantiateClass))
96+
.toList();
97+
ReflectiveProcessor processorToUse = (processors.size() == 1 ? processors.get(0)
98+
: new DelegateReflectiveProcessor(processors));
99+
return new Entry(element, processorToUse);
100+
}
101+
102+
static class DelegateReflectiveProcessor implements ReflectiveProcessor {
103+
104+
private final Iterable<ReflectiveProcessor> processors;
105+
106+
public DelegateReflectiveProcessor(Iterable<ReflectiveProcessor> processors) {
107+
this.processors = processors;
108+
}
109+
110+
@Override
111+
public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
112+
this.processors.forEach(processor -> processor.registerReflectionHints(hints, element));
113+
}
114+
115+
}
116+
117+
private record Entry(AnnotatedElement element, ReflectiveProcessor processor) {}
118+
119+
private static class ReflectiveProcessorBeanRegistrationAotContribution implements BeanRegistrationAotContribution {
120+
121+
private static final Consumer<Builder> ANNOTATION_CUSTOMIZATIONS = hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS);
122+
123+
private final Iterable<Entry> entries;
124+
125+
public ReflectiveProcessorBeanRegistrationAotContribution(Iterable<Entry> entries) {
126+
this.entries = entries;
127+
}
128+
129+
@Override
130+
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
131+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
132+
runtimeHints.reflection().registerType(Reflective.class, ANNOTATION_CUSTOMIZATIONS);
133+
this.entries.forEach(entry -> {
134+
AnnotatedElement element = entry.element();
135+
entry.processor().registerReflectionHints(runtimeHints.reflection(), element);
136+
registerAnnotationIfNecessary(runtimeHints, element);
137+
});
138+
}
139+
140+
private void registerAnnotationIfNecessary(RuntimeHints hints, AnnotatedElement element) {
141+
MergedAnnotation<Reflective> reflectiveAnnotation = MergedAnnotations.from(element).get(Reflective.class);
142+
if (reflectiveAnnotation.getDistance() > 0) {
143+
RuntimeHintsUtils.registerAnnotation(hints, reflectiveAnnotation.getRoot());
144+
}
145+
}
146+
147+
}
148+
149+
}
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.context.aot.ReflectiveProcessorBeanRegistrationAotProcessor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.context.aot;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.aot.generate.DefaultGenerationContext;
28+
import org.springframework.aot.generate.GenerationContext;
29+
import org.springframework.aot.generate.InMemoryGeneratedFiles;
30+
import org.springframework.aot.hint.MemberCategory;
31+
import org.springframework.aot.hint.RuntimeHints;
32+
import org.springframework.aot.hint.TypeReference;
33+
import org.springframework.aot.hint.annotation.Reflective;
34+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
35+
import org.springframework.beans.factory.aot.BeanRegistrationCode;
36+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
37+
import org.springframework.beans.factory.support.RegisteredBean;
38+
import org.springframework.beans.factory.support.RootBeanDefinition;
39+
import org.springframework.core.annotation.AliasFor;
40+
import org.springframework.core.annotation.SynthesizedAnnotation;
41+
import org.springframework.lang.Nullable;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.mockito.Mockito.mock;
45+
46+
/**
47+
* Tests for {@link ReflectiveProcessorBeanRegistrationAotProcessor}.
48+
*
49+
* @author Stephane Nicoll
50+
*/
51+
class ReflectiveProcessorBeanRegistrationAotProcessorTests {
52+
53+
private final ReflectiveProcessorBeanRegistrationAotProcessor processor = new ReflectiveProcessorBeanRegistrationAotProcessor();
54+
55+
private final GenerationContext generationContext = new DefaultGenerationContext(
56+
new InMemoryGeneratedFiles());
57+
58+
@Test
59+
void shouldIgnoreNonAnnotatedType() {
60+
assertThat(createContribution(String.class)).isNull();
61+
}
62+
63+
@Test
64+
void shouldProcessAnnotationOnType() {
65+
process(SampleTypeAnnotatedBean.class);
66+
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleTypeAnnotatedBean.class))
67+
.isNotNull();
68+
}
69+
70+
@Test
71+
void shouldProcessAnnotationOnConstructor() {
72+
process(SampleConstructorAnnotatedBean.class);
73+
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleConstructorAnnotatedBean.class))
74+
.satisfies(typeHint -> assertThat(typeHint.constructors()).singleElement()
75+
.satisfies(constructorHint -> assertThat(constructorHint.getParameterTypes())
76+
.containsExactly(TypeReference.of(String.class))));
77+
}
78+
79+
@Test
80+
void shouldProcessAnnotationOnField() {
81+
process(SampleFieldAnnotatedBean.class);
82+
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleFieldAnnotatedBean.class))
83+
.satisfies(typeHint -> assertThat(typeHint.fields()).singleElement()
84+
.satisfies(fieldHint -> assertThat(fieldHint.getName()).isEqualTo("managed")));
85+
}
86+
87+
@Test
88+
void shouldProcessAnnotationOnMethod() {
89+
process(SampleMethodAnnotatedBean.class);
90+
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleMethodAnnotatedBean.class))
91+
.satisfies(typeHint -> assertThat(typeHint.methods()).singleElement()
92+
.satisfies(methodHint -> assertThat(methodHint.getName()).isEqualTo("managed")));
93+
}
94+
95+
@Test
96+
void shouldRegisterAnnotation() {
97+
process(SampleMethodMetaAnnotatedBean.class);
98+
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
99+
assertThat(runtimeHints.reflection().getTypeHint(SampleInvoker.class)).satisfies(typeHint ->
100+
assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS));
101+
assertThat(runtimeHints.proxies().jdkProxies()).isEmpty();
102+
}
103+
104+
@Test
105+
void shouldRegisterAnnotationAndProxyWithAliasFor() {
106+
process(SampleMethodMetaAnnotatedBeanWithAlias.class);
107+
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
108+
assertThat(runtimeHints.reflection().getTypeHint(RetryInvoker.class)).satisfies(typeHint ->
109+
assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS));
110+
assertThat(runtimeHints.proxies().jdkProxies()).anySatisfy(jdkProxyHint ->
111+
assertThat(jdkProxyHint.getProxiedInterfaces()).containsExactly(
112+
TypeReference.of(RetryInvoker.class), TypeReference.of(SynthesizedAnnotation.class)));
113+
}
114+
115+
@Nullable
116+
private BeanRegistrationAotContribution createContribution(Class<?> beanClass) {
117+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
118+
beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass));
119+
return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName()));
120+
}
121+
122+
private void process(Class<?> beanClass) {
123+
BeanRegistrationAotContribution contribution = createContribution(beanClass);
124+
assertThat(contribution).isNotNull();
125+
contribution.applyTo(this.generationContext, mock(BeanRegistrationCode.class));
126+
}
127+
128+
@Reflective
129+
@SuppressWarnings("unused")
130+
static class SampleTypeAnnotatedBean {
131+
132+
private String notManaged;
133+
134+
public void notManaged() {
135+
136+
}
137+
}
138+
139+
@SuppressWarnings("unused")
140+
static class SampleConstructorAnnotatedBean {
141+
142+
@Reflective
143+
SampleConstructorAnnotatedBean(String name) {
144+
145+
}
146+
147+
SampleConstructorAnnotatedBean(Integer nameAsNumber) {
148+
149+
}
150+
151+
}
152+
153+
@SuppressWarnings("unused")
154+
static class SampleFieldAnnotatedBean {
155+
156+
@Reflective
157+
String managed;
158+
159+
String notManaged;
160+
161+
}
162+
163+
@SuppressWarnings("unused")
164+
static class SampleMethodAnnotatedBean {
165+
166+
@Reflective
167+
void managed() {
168+
}
169+
170+
void notManaged() {
171+
}
172+
173+
}
174+
175+
@SuppressWarnings("unused")
176+
static class SampleMethodMetaAnnotatedBean {
177+
178+
@SampleInvoker
179+
void invoke() {
180+
}
181+
182+
void notManaged() {
183+
}
184+
185+
}
186+
187+
@SuppressWarnings("unused")
188+
static class SampleMethodMetaAnnotatedBeanWithAlias {
189+
190+
@RetryInvoker
191+
void invoke() {
192+
}
193+
194+
void notManaged() {
195+
}
196+
197+
}
198+
199+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
200+
@Retention(RetentionPolicy.RUNTIME)
201+
@Documented
202+
@Reflective
203+
@interface SampleInvoker {
204+
205+
int retries() default 0;
206+
207+
}
208+
209+
@Target({ ElementType.METHOD })
210+
@Retention(RetentionPolicy.RUNTIME)
211+
@Documented
212+
@SampleInvoker
213+
@interface RetryInvoker {
214+
215+
@AliasFor(attribute = "retries", annotation = SampleInvoker.class)
216+
int value() default 1;
217+
218+
}
219+
220+
}

0 commit comments

Comments
 (0)