Browse Source

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
pull/33150/head
Stéphane Nicoll 2 years ago
parent
commit
f4607da45f
  1. 2
      spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java
  2. 100
      spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java
  3. 49
      spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java
  4. 29
      spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessor.java
  5. 99
      spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java
  6. 21
      spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java
  7. 18
      spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java
  8. 186
      spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java

2
spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java

@ -35,8 +35,8 @@ import org.springframework.core.annotation.AliasFor; @@ -35,8 +35,8 @@ import org.springframework.core.annotation.AliasFor;
* @author Stephane Nicoll
* @author Sam Brannen
* @since 6.0
* @see SimpleReflectiveProcessor
* @see ReflectiveRuntimeHintsRegistrar
* @see RegisterReflection @RegisterReflection
* @see RegisterReflectionForBinding @RegisterReflectionForBinding
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})

100
spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
/*
* 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.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.aot.hint.MemberCategory;
/**
* Register reflection hints against an arbitrary number of target classes.
*
* <p>When using this annotation directly, only the defined
* {@linkplain #memberCategories() member categories} are registered for each
* target class. The target classes can be specified by class or class names.
* When both are specified, they are all considered. If no target class is
* specified, the current class is used.
*
* <p>This annotation can be used as a meta-annotation to customize how hints
* are registered against each target class.
*
* <p>The annotated element can be any bean:
* <pre><code class="java">
* &#064;Configuration
* &#064;RegisterReflection(classes = CustomerEntry.class, memberCategories = PUBLIC_FIELDS)
* public class MyConfig {
* // ...
* }</code></pre>
*
* <p>To register reflection hints for the type itself, only member categories
* should be specified:<pre><code class="java">
* &#064;Component
* &#064;RegisterReflection(memberCategories = INVOKE_PUBLIC_METHODS)
* public class MyComponent {
* // ...
* }</code></pre>
*
* <p>Reflection hints can be registered from a method. In this case, at least
* one target class should be specified:<pre><code class="java">
* &#064;Component
* public class MyComponent {
*
* &#064;RegisterReflection(classes = CustomerEntry.class, memberCategories = PUBLIC_FIELDS)
* CustomerEntry process() { ... }
* // ...
* }</code></pre>
*
* <p>If the class is not available, {@link #classNames()} allows to specify the
* fully qualified name, rather than the {@link Class} reference.
*
* @author Stephane Nicoll
* @since 6.2
*/
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective(RegisterReflectionReflectiveProcessor.class)
public @interface RegisterReflection {
/**
* Classes for which reflection hints should be registered. Consider using
* {@link #classNames()} for classes that are not public in the current
* scope. If both {@code classes} and {@code classNames} are specified, they
* are merged in a single set.
* <p>
* By default, the annotated type is the target of the registration. When
* placed on a method, at least one class must be specified.
* @see #classNames()
*/
Class<?>[] classes() default {};
/**
* Alternative to {@link #classes()} to specify the classes as class names.
* @see #classes()
*/
String[] classNames() default {};
/**
* Specify the {@linkplain MemberCategory member categories} to enable.
*/
MemberCategory[] memberCategories() default {};
}

49
spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java

@ -25,53 +25,61 @@ import java.lang.annotation.Target; @@ -25,53 +25,61 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Indicates that the classes specified in the annotation attributes require some
* reflection hints for binding or reflection-based serialization purposes. For each
* class specified, hints on constructors, fields, properties, record components,
* including types transitively used on properties and record components are registered.
* At least one class must be specified in the {@code value} or {@code classes} annotation
* attributes.
* Register reflection hints for data binding or reflection-based serialization
* against an arbitrary number of target classes.
*
* <p>The annotated element can be a configuration class &mdash; for example:
* <p>For each class hints are registered for constructors, fields, properties,
* and record components. Hints are also registered for types transitively used
* on properties and record components.
*
* <pre class="code">
* <p>The annotated element can be a configuration class &mdash; for example:
* <pre><code class="java">
* &#064;Configuration
* &#064;RegisterReflectionForBinding({Foo.class, Bar.class})
* public class MyConfig {
* // ...
* }</pre>
* }</code></pre>
*
* <p>The annotated element can be any Spring bean class or method &mdash; for example:
* <p>When the annotated element is a type, the type itself is registered if no
* candidates are provided:<pre><code class="java">
* &#064;Component
* &#064;RegisterReflectionForBinding
* public class MyBean {
* // ...
* }</code></pre>
*
* <pre class="code">
* &#064;Service
* The annotation can also be specified on a method. In that case, at least one
* target class must be specified:<pre><code class="java">
* &#064;Component
* public class MyService {
*
* &#064;RegisterReflectionForBinding(Baz.class)
* public void process() {
* public Baz process() {
* // ...
* }
*
* }</pre>
* }</code></pre>
*
* <p>The annotated element can also be any test class that uses the <em>Spring
* TestContext Framework</em> to load an {@code ApplicationContext}.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
* @since 6.0
* @see org.springframework.aot.hint.BindingReflectionHintsRegistrar
* @see Reflective @Reflective
* @see RegisterReflection @RegisterReflection
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RegisterReflection
@Reflective(RegisterReflectionForBindingProcessor.class)
public @interface RegisterReflectionForBinding {
/**
* Alias for {@link #classes()}.
*/
@AliasFor("classes")
@AliasFor(annotation = RegisterReflection.class, attribute = "classes")
Class<?>[] value() default {};
/**
@ -79,7 +87,14 @@ public @interface RegisterReflectionForBinding { @@ -79,7 +87,14 @@ public @interface RegisterReflectionForBinding {
* <p>At least one class must be specified either via {@link #value} or {@code classes}.
* @see #value()
*/
@AliasFor("value")
@AliasFor(annotation = RegisterReflection.class, attribute = "classes")
Class<?>[] classes() default {};
/**
* Alternative to {@link #classes()} to specify the classes as class names.
* @see #classes()
*/
@AliasFor(annotation = RegisterReflection.class, attribute = "classNames")
String[] classNames() default {};
}

29
spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessor.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@ -16,39 +16,28 @@ @@ -16,39 +16,28 @@
package org.springframework.aot.hint.annotation;
import java.lang.reflect.AnnotatedElement;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
/**
* A {@link ReflectiveProcessor} implementation that registers reflection hints
* for data binding purpose (class, constructors, fields, properties, record
* components, including types transitively used on properties and record components).
* for data binding purpose, that is class, constructors, fields, properties,
* record components, including types transitively used on properties and record
* components.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
* @since 6.0
* @see RegisterReflectionForBinding @RegisterReflectionForBinding
*/
public class RegisterReflectionForBindingProcessor implements ReflectiveProcessor {
class RegisterReflectionForBindingProcessor extends RegisterReflectionReflectiveProcessor {
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
RegisterReflectionForBinding registerReflection =
AnnotationUtils.getAnnotation(element, RegisterReflectionForBinding.class);
if (registerReflection != null) {
Class<?>[] classes = registerReflection.classes();
Assert.state(classes.length != 0, () -> "A least one class should be specified in " +
"@RegisterReflectionForBinding attributes, and none was provided on " + element);
for (Class<?> type : classes) {
this.bindingRegistrar.registerReflectionHints(hints, type);
}
}
protected void registerReflectionHints(ReflectionHints hints, Class<?> target, MemberCategory[] memberCategories) {
this.bindingRegistrar.registerReflectionHints(hints, target);
}
}

99
spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A {@link ReflectiveProcessor} implementation that pairs with
* {@link RegisterReflection @RegisterReflection}. Can be used as a base
* implementation for composed annotations that are meta-annotated with
* {@link RegisterReflection}.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class RegisterReflectionReflectiveProcessor implements ReflectiveProcessor {
private static final Log logger = LogFactory.getLog(RegisterReflectionReflectiveProcessor.class);
@Override
public final void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
RegisterReflection annotation = AnnotatedElementUtils.getMergedAnnotation(
element, RegisterReflection.class);
Assert.notNull(annotation, "Element must be annotated with @" + RegisterReflection.class.getSimpleName()
+ ": " + element);
ReflectionRegistration registration = parse(element, annotation);
registerReflectionHints(hints, registration);
}
protected ReflectionRegistration parse(AnnotatedElement element, RegisterReflection annotation) {
List<Class<?>> allClassNames = new ArrayList<>();
allClassNames.addAll(Arrays.asList(annotation.classes()));
allClassNames.addAll(Arrays.stream(annotation.classNames())
.map(this::loadClass).filter(Objects::nonNull).toList());
if (allClassNames.isEmpty()) {
if (element instanceof Class<?> clazz) {
allClassNames.add(clazz);
}
else {
throw new IllegalStateException("At least one class must be specified, "
+ "could not detect target from '" + element + "'");
}
}
return new ReflectionRegistration(allClassNames.toArray(new Class<?>[0]),
annotation.memberCategories());
}
protected void registerReflectionHints(ReflectionHints hints, ReflectionRegistration registration) {
for (Class<?> target : registration.classes) {
registerReflectionHints(hints, target, registration.memberCategories);
}
}
protected void registerReflectionHints(ReflectionHints hints, Class<?> target, MemberCategory[] memberCategories) {
hints.registerType(target, type -> type.withMembers(memberCategories));
}
@Nullable
private Class<?> loadClass(String className) {
try {
return ClassUtils.forName(className, getClass().getClassLoader());
}
catch (Exception ex) {
logger.warn("Ignoring '" + className + "': " + ex.getMessage());
return null;
}
}
protected record ReflectionRegistration(Class<?>[] classes, MemberCategory[] memberCategories) {}
}

21
spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java

@ -25,6 +25,7 @@ import java.lang.reflect.Method; @@ -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 { @@ -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 { @@ -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 {

18
spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 {

186
spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java

@ -0,0 +1,186 @@ @@ -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;
}
}
}
Loading…
Cancel
Save