Introduce RegisterReflection

This commit introduces a declarative way of registering reflection
information for arbitrary types. Types can be specified as a class,
a class name, or by annotating the type itself.

This existing RegisterReflectionForBinding becomes a specialized
version of the new annotation, registering the necessary hints for
data binding.

Closes gh-29194
This commit is contained in:
Stéphane Nicoll
2024-05-02 16:18:17 +02:00
parent b0807d847e
commit f4607da45f
8 changed files with 460 additions and 44 deletions

View File

@@ -25,6 +25,7 @@ import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.FieldHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
@@ -118,6 +119,18 @@ class ReflectiveRuntimeHintsRegistrarTests {
.satisfies(methodHint -> assertThat(methodHint.getName()).isEqualTo("managed")));
}
@Test
void shouldProcessDifferentAnnotationsOnTypeAndField() {
process(SampleTypeAndFieldAnnotatedBean.class);
assertThat(this.runtimeHints.reflection().getTypeHint(SampleTypeAndFieldAnnotatedBean.class))
.satisfies(typeHint -> {
assertThat(typeHint.fields().map(FieldHint::getName)).containsOnly("MESSAGE");
assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
assertThat(typeHint.methods()).isEmpty();
assertThat(typeHint.constructors()).isEmpty();
});
}
@Test
void shouldInvokeCustomProcessor() {
process(SampleCustomProcessor.class);
@@ -175,6 +188,14 @@ class ReflectiveRuntimeHintsRegistrarTests {
}
}
@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
static class SampleTypeAndFieldAnnotatedBean {
@Reflective
private static final String MESSAGE = "Hello";
}
@SuppressWarnings("unused")
static class SampleMethodMetaAnnotatedBean {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* Tests for {@link RegisterReflectionForBindingProcessor}.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
*/
class RegisterReflectionForBindingProcessorTests {
@@ -52,10 +53,11 @@ class RegisterReflectionForBindingProcessorTests {
}
@Test
void throwExceptionWithoutAnnotationAttributeOnClass() {
assertThatThrownBy(() -> processor.registerReflectionHints(hints.reflection(),
SampleClassWithoutAnnotationAttribute.class))
.isInstanceOf(IllegalStateException.class);
void registerReflectionForBindingOnClassItself() {
processor.registerReflectionHints(hints.reflection(), SampleClassWithoutAnnotationAttribute.class);
assertThat(RuntimeHintsPredicates.reflection().onType(SampleClassWithoutAnnotationAttribute.class)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onType(String.class)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithoutAnnotationAttribute.class, "getName")).accepts(hints);
}
@Test
@@ -80,12 +82,16 @@ class RegisterReflectionForBindingProcessorTests {
static class SampleClassWithGetter {
public String getName() {
return null;
return "test";
}
}
@RegisterReflectionForBinding
static class SampleClassWithoutAnnotationAttribute {
public String getName() {
return "test";
}
}
static class SampleClassWithoutMethodLevelAnnotationAttribute {

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2002-2024 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.Method;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.ExecutableHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeHint;
import org.springframework.aot.hint.TypeReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link RegisterReflectionReflectiveProcessor}.
*
* @author Stephane Nicoll
*/
class RegisterReflectionReflectiveProcessorTests {
private static final List<String> NO_METHODS = Collections.emptyList();
private final RegisterReflectionReflectiveProcessor processor = new RegisterReflectionReflectiveProcessor();
private final RuntimeHints hints = new RuntimeHints();
@Nested
class AnnotatedTypeTests {
@Test
void registerReflectionWithMemberCategory() {
registerReflectionHints(RegistrationSimple.class);
assertBasicTypeHint(SimplePojo.class, NO_METHODS, List.of(MemberCategory.INVOKE_PUBLIC_METHODS));
}
@Test
void registerReflectionForMultipleTargets() {
registerReflectionHints(RegistrationMultipleTargets.class);
assertThat(hints.reflection().typeHints()).allSatisfy(
hasOnlyMemberCategories(MemberCategory.INVOKE_PUBLIC_METHODS));
assertThat(hints.reflection().typeHints().map(TypeHint::getType))
.hasSameElementsAs(TypeReference.listOf(Number.class, Double.class, Integer.class, Float.class));
}
@Test
void registerReflectionOnTargetClass() {
registerReflectionHints(AnnotatedSimplePojo.class);
assertBasicTypeHint(AnnotatedSimplePojo.class, NO_METHODS,
List.of(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
}
}
@Nested
class AnnotatedMethodTests {
@Test
void registerReflectionForStaticField() throws NoSuchMethodException {
Method method = RegistrationMethod.class.getDeclaredMethod("doReflection");
registerReflectionHints(method);
assertBasicTypeHint(SimplePojo.class, NO_METHODS, List.of(MemberCategory.INVOKE_DECLARED_METHODS));
}
@Test
void registerReflectionWithoutTarget() throws NoSuchMethodException {
Method method = RegistrationMethodWithoutTarget.class.getDeclaredMethod("doReflection");
assertThatIllegalStateException()
.isThrownBy(() -> registerReflectionHints(method))
.withMessageContaining("At least one class must be specified, could not detect target from '")
.withMessageContaining(method.toString());
}
}
private void assertBasicTypeHint(Class<?> type, List<String> methodNames, List<MemberCategory> memberCategories) {
TypeHint typeHint = getTypeHint(type);
assertThat(typeHint.methods()).map(ExecutableHint::getName).hasSameElementsAs(methodNames);
assertThat(typeHint.getMemberCategories()).hasSameElementsAs(memberCategories);
assertThat(typeHint.fields()).isEmpty();
assertThat(typeHint.constructors()).isEmpty();
}
private Consumer<TypeHint> hasOnlyMemberCategories(MemberCategory... categories) {
return typeHint -> {
assertThat(typeHint.fields()).isEmpty();
assertThat(typeHint.methods()).isEmpty();
assertThat(typeHint.constructors()).isEmpty();
assertThat(typeHint.getMemberCategories()).containsOnly(categories);
};
}
private TypeHint getTypeHint(Class<?> target) {
TypeHint typeHint = hints.reflection().getTypeHint(target);
assertThat(typeHint).isNotNull();
return typeHint;
}
private void registerReflectionHints(AnnotatedElement annotatedElement) {
this.processor.registerReflectionHints(this.hints.reflection(), annotatedElement);
}
@RegisterReflection(classes = SimplePojo.class, memberCategories = MemberCategory.INVOKE_PUBLIC_METHODS)
static class RegistrationSimple {}
@RegisterReflection(classes = { Number.class, Double.class },
classNames = { "java.lang.Integer", "java.lang.Float" }, memberCategories = MemberCategory.INVOKE_PUBLIC_METHODS)
static class RegistrationMultipleTargets {
}
static class RegistrationMethod {
@RegisterReflection(classes = SimplePojo.class, memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)
private void doReflection() {
}
}
static class RegistrationMethodWithoutTarget {
@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
private void doReflection() {
}
}
static class SimplePojo {
private String name;
private String description;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
}
@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
static class AnnotatedSimplePojo {
private String test;
AnnotatedSimplePojo(String test) {
this.test = test;
}
}
}