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
This commit is contained in:
Stephane Nicoll
2022-05-18 11:34:32 +02:00
parent 2517c72f7d
commit 4cca190aad
8 changed files with 684 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2002-2022 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.context.aot;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeHint.Builder;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
import org.springframework.aot.hint.support.RuntimeHintsUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
* AOT {@code BeanRegistrationAotProcessor} that detects the presence of
* {@link Reflective @Reflective} on annotated elements and invoke the
* underlying {@link ReflectiveProcessor} implementations.
*
* @author Stephane Nicoll
*/
class ReflectiveProcessorBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
private final Map<Class<? extends ReflectiveProcessor>, ReflectiveProcessor> processors = new HashMap<>();
@Nullable
@Override
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
Class<?> beanClass = registeredBean.getBeanClass();
Set<Entry> entries = new LinkedHashSet<>();
if (isReflective(beanClass)) {
entries.add(createEntry(beanClass));
}
doWithReflectiveConstructors(beanClass, constructor ->
entries.add(createEntry(constructor)));
ReflectionUtils.doWithFields(beanClass, field ->
entries.add(createEntry(field)), this::isReflective);
ReflectionUtils.doWithMethods(beanClass, method ->
entries.add(createEntry(method)), this::isReflective);
if (!entries.isEmpty()) {
return new ReflectiveProcessorBeanRegistrationAotContribution(entries);
}
return null;
}
private void doWithReflectiveConstructors(Class<?> beanClass, Consumer<Constructor<?>> consumer) {
for (Constructor<?> constructor : beanClass.getDeclaredConstructors()) {
if (isReflective(constructor)) {
consumer.accept(constructor);
}
}
}
private boolean isReflective(AnnotatedElement element) {
return MergedAnnotations.from(element).isPresent(Reflective.class);
}
@SuppressWarnings("unchecked")
private Entry createEntry(AnnotatedElement element) {
Class<? extends ReflectiveProcessor>[] processorClasses = (Class<? extends ReflectiveProcessor>[])
MergedAnnotations.from(element).get(Reflective.class).getClassArray("value");
List<ReflectiveProcessor> processors = Arrays.stream(processorClasses).distinct()
.map(processorClass -> this.processors.computeIfAbsent(processorClass, BeanUtils::instantiateClass))
.toList();
ReflectiveProcessor processorToUse = (processors.size() == 1 ? processors.get(0)
: new DelegateReflectiveProcessor(processors));
return new Entry(element, processorToUse);
}
static class DelegateReflectiveProcessor implements ReflectiveProcessor {
private final Iterable<ReflectiveProcessor> processors;
public DelegateReflectiveProcessor(Iterable<ReflectiveProcessor> processors) {
this.processors = processors;
}
@Override
public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
this.processors.forEach(processor -> processor.registerReflectionHints(hints, element));
}
}
private record Entry(AnnotatedElement element, ReflectiveProcessor processor) {}
private static class ReflectiveProcessorBeanRegistrationAotContribution implements BeanRegistrationAotContribution {
private static final Consumer<Builder> ANNOTATION_CUSTOMIZATIONS = hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS);
private final Iterable<Entry> entries;
public ReflectiveProcessorBeanRegistrationAotContribution(Iterable<Entry> entries) {
this.entries = entries;
}
@Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
runtimeHints.reflection().registerType(Reflective.class, ANNOTATION_CUSTOMIZATIONS);
this.entries.forEach(entry -> {
AnnotatedElement element = entry.element();
entry.processor().registerReflectionHints(runtimeHints.reflection(), element);
registerAnnotationIfNecessary(runtimeHints, element);
});
}
private void registerAnnotationIfNecessary(RuntimeHints hints, AnnotatedElement element) {
MergedAnnotation<Reflective> reflectiveAnnotation = MergedAnnotations.from(element).get(Reflective.class);
if (reflectiveAnnotation.getDistance() > 0) {
RuntimeHintsUtils.registerAnnotation(hints, reflectiveAnnotation.getRoot());
}
}
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor= \
org.springframework.context.aot.ReflectiveProcessorBeanRegistrationAotProcessor

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2002-2022 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.context.aot;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.SynthesizedAnnotation;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ReflectiveProcessorBeanRegistrationAotProcessor}.
*
* @author Stephane Nicoll
*/
class ReflectiveProcessorBeanRegistrationAotProcessorTests {
private final ReflectiveProcessorBeanRegistrationAotProcessor processor = new ReflectiveProcessorBeanRegistrationAotProcessor();
private final GenerationContext generationContext = new DefaultGenerationContext(
new InMemoryGeneratedFiles());
@Test
void shouldIgnoreNonAnnotatedType() {
assertThat(createContribution(String.class)).isNull();
}
@Test
void shouldProcessAnnotationOnType() {
process(SampleTypeAnnotatedBean.class);
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleTypeAnnotatedBean.class))
.isNotNull();
}
@Test
void shouldProcessAnnotationOnConstructor() {
process(SampleConstructorAnnotatedBean.class);
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleConstructorAnnotatedBean.class))
.satisfies(typeHint -> assertThat(typeHint.constructors()).singleElement()
.satisfies(constructorHint -> assertThat(constructorHint.getParameterTypes())
.containsExactly(TypeReference.of(String.class))));
}
@Test
void shouldProcessAnnotationOnField() {
process(SampleFieldAnnotatedBean.class);
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleFieldAnnotatedBean.class))
.satisfies(typeHint -> assertThat(typeHint.fields()).singleElement()
.satisfies(fieldHint -> assertThat(fieldHint.getName()).isEqualTo("managed")));
}
@Test
void shouldProcessAnnotationOnMethod() {
process(SampleMethodAnnotatedBean.class);
assertThat(this.generationContext.getRuntimeHints().reflection().getTypeHint(SampleMethodAnnotatedBean.class))
.satisfies(typeHint -> assertThat(typeHint.methods()).singleElement()
.satisfies(methodHint -> assertThat(methodHint.getName()).isEqualTo("managed")));
}
@Test
void shouldRegisterAnnotation() {
process(SampleMethodMetaAnnotatedBean.class);
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
assertThat(runtimeHints.reflection().getTypeHint(SampleInvoker.class)).satisfies(typeHint ->
assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS));
assertThat(runtimeHints.proxies().jdkProxies()).isEmpty();
}
@Test
void shouldRegisterAnnotationAndProxyWithAliasFor() {
process(SampleMethodMetaAnnotatedBeanWithAlias.class);
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
assertThat(runtimeHints.reflection().getTypeHint(RetryInvoker.class)).satisfies(typeHint ->
assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS));
assertThat(runtimeHints.proxies().jdkProxies()).anySatisfy(jdkProxyHint ->
assertThat(jdkProxyHint.getProxiedInterfaces()).containsExactly(
TypeReference.of(RetryInvoker.class), TypeReference.of(SynthesizedAnnotation.class)));
}
@Nullable
private BeanRegistrationAotContribution createContribution(Class<?> beanClass) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass));
return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName()));
}
private void process(Class<?> beanClass) {
BeanRegistrationAotContribution contribution = createContribution(beanClass);
assertThat(contribution).isNotNull();
contribution.applyTo(this.generationContext, mock(BeanRegistrationCode.class));
}
@Reflective
@SuppressWarnings("unused")
static class SampleTypeAnnotatedBean {
private String notManaged;
public void notManaged() {
}
}
@SuppressWarnings("unused")
static class SampleConstructorAnnotatedBean {
@Reflective
SampleConstructorAnnotatedBean(String name) {
}
SampleConstructorAnnotatedBean(Integer nameAsNumber) {
}
}
@SuppressWarnings("unused")
static class SampleFieldAnnotatedBean {
@Reflective
String managed;
String notManaged;
}
@SuppressWarnings("unused")
static class SampleMethodAnnotatedBean {
@Reflective
void managed() {
}
void notManaged() {
}
}
@SuppressWarnings("unused")
static class SampleMethodMetaAnnotatedBean {
@SampleInvoker
void invoke() {
}
void notManaged() {
}
}
@SuppressWarnings("unused")
static class SampleMethodMetaAnnotatedBeanWithAlias {
@RetryInvoker
void invoke() {
}
void notManaged() {
}
}
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
@interface SampleInvoker {
int retries() default 0;
}
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SampleInvoker
@interface RetryInvoker {
@AliasFor(attribute = "retries", annotation = SampleInvoker.class)
int value() default 1;
}
}