From 4cca190aad3cf643c765f39655432e0ebd00549e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 18 May 2022 11:34:32 +0200 Subject: [PATCH] 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 --- ...ProcessorBeanRegistrationAotProcessor.java | 149 ++++++++++++ .../resources/META-INF/spring/aot.factories | 2 + ...ssorBeanRegistrationAotProcessorTests.java | 220 ++++++++++++++++++ .../aot/hint/annotation/Reflective.java | 57 +++++ .../hint/annotation/ReflectiveProcessor.java | 39 ++++ .../annotation/SimpleReflectiveProcessor.java | 93 ++++++++ .../aot/hint/annotation/package-info.java | 9 + .../SimpleReflectiveProcessorTests.java | 115 +++++++++ 8 files changed, 684 insertions(+) create mode 100644 spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessor.java create mode 100644 spring-context/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java create mode 100644 spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java create mode 100644 spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveProcessor.java create mode 100644 spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java create mode 100644 spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java create mode 100644 spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessor.java new file mode 100644 index 0000000000..ec100046c0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessor.java @@ -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, ReflectiveProcessor> processors = new HashMap<>(); + + @Nullable + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + Set 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> 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[] processorClasses = (Class[]) + MergedAnnotations.from(element).get(Reflective.class).getClassArray("value"); + List 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 processors; + + public DelegateReflectiveProcessor(Iterable 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 ANNOTATION_CUSTOMIZATIONS = hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + + private final Iterable entries; + + public ReflectiveProcessorBeanRegistrationAotContribution(Iterable 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 reflectiveAnnotation = MergedAnnotations.from(element).get(Reflective.class); + if (reflectiveAnnotation.getDistance() > 0) { + RuntimeHintsUtils.registerAnnotation(hints, reflectiveAnnotation.getRoot()); + } + } + + } + +} diff --git a/spring-context/src/main/resources/META-INF/spring/aot.factories b/spring-context/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 0000000000..67939a421d --- /dev/null +++ b/spring-context/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor= \ +org.springframework.context.aot.ReflectiveProcessorBeanRegistrationAotProcessor \ No newline at end of file diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java new file mode 100644 index 0000000000..62bf86e38f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java @@ -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; + + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java new file mode 100644 index 0000000000..b8958fe593 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java @@ -0,0 +1,57 @@ +/* + * 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.aot.hint.annotation; + +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.springframework.core.annotation.AliasFor; + +/** + * Indicate that the annotated element requires reflection. + * + *

When present, either directly or as a meta-annotation, this annotation + * triggers the configured {@linkplain ReflectiveProcessor processors} against + * the annotated element. By default, a reflection hint is added on the + * annotated element so that it can be discovered and invoked if necessary. + * + * @author Stephane Nicoll + * @since 6.0 + * @see SimpleReflectiveProcessor + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, + ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Reflective { + + /** + * Alias for {@link #processors()}. + */ + Class[] value() default SimpleReflectiveProcessor.class; + + /** + * {@link ReflectiveProcessor} implementations to invoke against the + * annotated element. + */ + @AliasFor("value") + Class[] processors() default SimpleReflectiveProcessor.class; + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveProcessor.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveProcessor.java new file mode 100644 index 0000000000..a753d38562 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveProcessor.java @@ -0,0 +1,39 @@ +/* + * 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.aot.hint.annotation; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.aot.hint.ReflectionHints; + +/** + * Process an {@link AnnotatedElement} and register the necessary reflection + * hints for it. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public interface ReflectiveProcessor { + + /** + * Register {@link ReflectionHints} against the specified {@link AnnotatedElement}. + * @param hints the reflection hints instance to use + * @param element the element to process + */ + void registerReflectionHints(ReflectionHints hints, AnnotatedElement element); + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java new file mode 100644 index 0000000000..4ad5e9363d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java @@ -0,0 +1,93 @@ +/* + * 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.aot.hint.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Consumer; + +import org.springframework.aot.hint.ExecutableHint.Builder; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; + +/** + * A simple {@link ReflectiveProcessor} implementation that registers only a + * reflection hint for the annotated type. Can be sub-classed to customize + * processing for a given {@link AnnotatedElement} type. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class SimpleReflectiveProcessor implements ReflectiveProcessor { + + private static final Consumer INVOKE_EXECUTABLE = hint -> hint.setModes(ExecutableMode.INVOKE); + + @Override + public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { + if (element instanceof Class type) { + registerTypeHint(hints, type); + } + else if (element instanceof Constructor constructor) { + registerConstructorHint(hints, constructor); + } + else if (element instanceof Field field) { + registerFieldHint(hints, field); + } + else if (element instanceof Method method) { + registerMethodHint(hints, method); + } + } + + /** + * Register {@link ReflectionHints} against the specified {@link Class}. + * @param hints the reflection hints instance to use + * @param type the class to process + */ + protected void registerTypeHint(ReflectionHints hints, Class type) { + hints.registerType(type, hint -> {}); + } + + /** + * Register {@link ReflectionHints} against the specified {@link Constructor}. + * @param hints the reflection hints instance to use + * @param constructor the constructor to process + */ + protected void registerConstructorHint(ReflectionHints hints, Constructor constructor) { + hints.registerConstructor(constructor, INVOKE_EXECUTABLE); + } + + /** + * Register {@link ReflectionHints} against the specified {@link Field}. + * @param hints the reflection hints instance to use + * @param field the field to process + */ + protected void registerFieldHint(ReflectionHints hints, Field field) { + hints.registerField(field); + } + + /** + * Register {@link ReflectionHints} against the specified {@link Method}. + * @param hints the reflection hints instance to use + * @param method the method to process + */ + protected void registerMethodHint(ReflectionHints hints, Method method) { + hints.registerMethod(method, INVOKE_EXECUTABLE); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java new file mode 100644 index 0000000000..fac7035752 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Annotation support for runtime hints. + */ +@NonNullApi +@NonNullFields +package org.springframework.aot.hint.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java new file mode 100644 index 0000000000..41ffe5141a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java @@ -0,0 +1,115 @@ +/* + * 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.aot.hint.annotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.TypeReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SimpleReflectiveProcessor}. + * + * @author Stephane Nicoll + */ +class SimpleReflectiveProcessorTests { + + private final SimpleReflectiveProcessor processor = new SimpleReflectiveProcessor(); + + private final ReflectionHints hints = new ReflectionHints(); + + @Test + void registerReflectiveHintsForClass() { + processor.registerReflectionHints(hints, SampleBean.class); + assertThat(hints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleBean.class)); + assertThat(typeHint.getMemberCategories()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.methods()).isEmpty(); + }); + } + + @Test + void registerReflectiveHintsForConstructor() { + Constructor constructor = SampleBean.class.getDeclaredConstructors()[0]; + processor.registerReflectionHints(hints, constructor); + assertThat(hints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleBean.class)); + assertThat(typeHint.getMemberCategories()).isEmpty(); + assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint -> { + assertThat(constructorHint.getName()).isEqualTo(""); + assertThat(constructorHint.getModes()).containsExactly(ExecutableMode.INVOKE); + assertThat(constructorHint.getParameterTypes()).containsExactly(TypeReference.of(String.class)); + }); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.methods()).isEmpty(); + }); + } + + @Test + void registerReflectiveHintsForField() throws NoSuchFieldException { + Field field = SampleBean.class.getDeclaredField("name"); + processor.registerReflectionHints(hints, field); + assertThat(hints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleBean.class)); + assertThat(typeHint.getMemberCategories()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> + assertThat(fieldHint.getName()).isEqualTo("name")); + assertThat(typeHint.methods()).isEmpty(); + }); + } + + @Test + void registerReflectiveHintsForMethod() throws NoSuchMethodException { + Method method = SampleBean.class.getDeclaredMethod("setName", String.class); + processor.registerReflectionHints(hints, method); + assertThat(hints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleBean.class)); + assertThat(typeHint.getMemberCategories()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("setName"); + assertThat(methodHint.getModes()).containsExactly(ExecutableMode.INVOKE); + assertThat(methodHint.getParameterTypes()).containsExactly(TypeReference.of(String.class)); + }); + }); + } + + static class SampleBean { + + private String name; + + SampleBean(String name) { + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + } + +}