Browse Source
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-29194pull/33150/head
8 changed files with 460 additions and 44 deletions
@ -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"> |
||||
* @Configuration |
||||
* @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"> |
||||
* @Component |
||||
* @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"> |
||||
* @Component |
||||
* public class MyComponent { |
||||
* |
||||
* @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 {}; |
||||
|
||||
} |
||||
@ -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) {} |
||||
|
||||
} |
||||
@ -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…
Reference in new issue