Browse Source

Add support for declaring reflection metadata for lambdas

Closes gh-36339
pull/36397/head
Stéphane Nicoll 4 weeks ago
parent
commit
3bc55c77ec
  1. 186
      spring-core/src/main/java/org/springframework/aot/hint/LambdaHint.java
  2. 38
      spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java
  3. 8
      spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java
  4. 28
      spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java
  5. 26
      spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java
  6. 69
      spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java

186
spring-core/src/main/java/org/springframework/aot/hint/LambdaHint.java

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
/*
* Copyright 2002-present 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
/**
* A hint that describes the need of reflection for a Lambda.
*
* @author Stephane Nicoll
* @since 7.0.6
*/
public final class LambdaHint implements ConditionalHint {
private final TypeReference declaringClass;
private final @Nullable TypeReference reachableType;
private final @Nullable DeclaringMethod declaringMethod;
private final List<TypeReference> interfaces;
private LambdaHint(Builder builder) {
this.declaringClass = builder.declaringClass;
this.reachableType = builder.reachableType;
this.declaringMethod = builder.declaringMethod;
this.interfaces = List.copyOf(builder.interfaces);
}
/**
* Initialize a builder with the class declaring the lambda.
* @param declaringClass the type declaring the lambda
* @return a builder for the hint
*/
public static Builder of(TypeReference declaringClass) {
return new Builder(declaringClass);
}
/**
* Initialize a builder with the class declaring the lambda.
* @param declaringClass the type declaring the lambda
* @return a builder for the hint
*/
public static Builder of(Class<?> declaringClass) {
return new Builder(TypeReference.of(declaringClass));
}
/**
* Return the type declaring the lambda.
* @return the declaring class
*/
public TypeReference getDeclaringClass() {
return this.declaringClass;
}
@Override
public @Nullable TypeReference getReachableType() {
return this.reachableType;
}
/**
* Return the method in which the lambda is defined, if any.
* @return the declaring method
*/
public @Nullable DeclaringMethod getDeclaringMethod() {
return this.declaringMethod;
}
/**
* Return the interfaces that are implemented by the lambda.
* @return the interfaces
*/
public List<TypeReference> getInterfaces() {
return this.interfaces;
}
public static class Builder {
private final TypeReference declaringClass;
private @Nullable TypeReference reachableType;
private @Nullable DeclaringMethod declaringMethod;
private final List<TypeReference> interfaces = new ArrayList<>();
Builder(TypeReference declaringClass) {
this.declaringClass = declaringClass;
}
/**
* Make this hint conditional on the fact that the specified type is in a
* reachable code path from a static analysis point of view.
* @param reachableType the type that should be reachable for this hint to apply
* @return {@code this}, to facilitate method chaining
*/
public Builder onReachableType(TypeReference reachableType) {
this.reachableType = reachableType;
return this;
}
/**
* Make this hint conditional on the fact that the specified type is in a
* reachable code path from a static analysis point of view.
* @param reachableType the type that should be reachable for this hint to apply
* @return {@code this}, to facilitate method chaining
*/
public Builder onReachableType(Class<?> reachableType) {
this.reachableType = TypeReference.of(reachableType);
return this;
}
/**
* Set the method that declares the lambda.
* @param name the name of the method
* @param parameterTypes the parameter types, if any.
* @return {@code this}, to facilitate method chaining
*/
public Builder withDeclaringMethod(String name, List<TypeReference> parameterTypes) {
this.declaringMethod = new DeclaringMethod(name, parameterTypes);
return this;
}
/**
* Set the method that declares the lambda.
* @param name the name of the method
* @param parameterTypes the parameter types, if any.
* @return {@code this}, to facilitate method chaining
*/
public Builder withDeclaringMethod(String name, Class<?>... parameterTypes) {
return withDeclaringMethod(name, Arrays.stream(parameterTypes).map(TypeReference::of).toList());
}
/**
* Add the specified interfaces that the lambda should implement.
* @param interfaces the interfaces the lambda should implement
* @return {@code this}, to facilitate method chaining
*/
public Builder withInterfaces(TypeReference... interfaces) {
this.interfaces.addAll(Arrays.asList(interfaces));
return this;
}
/**
* Add the specified interfaces that the lambda should implement.
* @param interfaces the interfaces the lambda should implement
* @return {@code this}, to facilitate method chaining
*/
public Builder withInterfaces(Class<?>... interfaces) {
this.interfaces.addAll(Arrays.stream(interfaces).map(TypeReference::of).toList());
return this;
}
public LambdaHint build() {
return new LambdaHint(this);
}
}
/**
* Describe a method.
* @param name the name of the method
* @param parameterTypes the parameter types
*/
public record DeclaringMethod(String name, List<TypeReference> parameterTypes) {
}
}

38
spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java

@ -21,8 +21,10 @@ import java.lang.reflect.Executable; @@ -21,8 +21,10 @@ import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
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 java.util.stream.Stream;
@ -45,6 +47,7 @@ public class ReflectionHints { @@ -45,6 +47,7 @@ public class ReflectionHints {
private final Map<TypeReference, TypeHint.Builder> types = new HashMap<>();
private final Set<LambdaHint> lambdaHints = new LinkedHashSet<>();
/**
* Return the types that require reflection.
@ -54,6 +57,16 @@ public class ReflectionHints { @@ -54,6 +57,16 @@ public class ReflectionHints {
return this.types.values().stream().map(TypeHint.Builder::build);
}
/**
* Return the lambda hints.
* @return a stream of {@link LambdaHint}
* @since 7.0.6
*/
public Stream<LambdaHint> lambdaHints() {
return this.lambdaHints.stream();
}
/**
* Return the reflection hints for the type defined by the specified
* {@link TypeReference}.
@ -242,6 +255,31 @@ public class ReflectionHints { @@ -242,6 +255,31 @@ public class ReflectionHints {
typeHint -> typeHint.withJavaSerialization(true));
}
/**
* Register a {@link LambdaHint}.
* @param declaringClass the type declaring the lambda
* @param lambdaHint the consumer of the hint builder
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerLambda(TypeReference declaringClass, Consumer<LambdaHint.Builder> lambdaHint) {
LambdaHint.Builder builder = LambdaHint.of(declaringClass);
lambdaHint.accept(builder);
this.lambdaHints.add(builder.build());
return this;
}
/**
* Register a {@link LambdaHint}.
* @param declaringClass the type declaring the lambda
* @param lambdaHint the consumer of the hint builder
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerLambda(Class<?> declaringClass, Consumer<LambdaHint.Builder> lambdaHint) {
return this.registerLambda(TypeReference.of(declaringClass), lambdaHint);
}
private List<TypeReference> mapParameters(Executable executable) {
return TypeReference.listOf(executable.getParameterTypes());
}

8
spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java

@ -19,10 +19,10 @@ package org.springframework.aot.hint; @@ -19,10 +19,10 @@ package org.springframework.aot.hint;
/**
* Gather hints that can be used to optimize the application runtime.
*
* <p>Use of reflection can be recorded for individual members of a type, as
* well as broader {@linkplain MemberCategory member categories}. Access to
* resources can be specified using patterns or the base name of a resource
* bundle.
* <p>Use of reflection can be recorded for individual members of a type,
* lambdas,as well as broader {@linkplain MemberCategory member categories}.
* Access to resources can be specified using patterns or the base name of a
* resource bundle.
*
* <p>Hints that require the need for Java serialization of proxies can be
* recorded as well.

28
spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java

@ -35,6 +35,7 @@ import org.springframework.aot.hint.ExecutableMode; @@ -35,6 +35,7 @@ import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.FieldHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.LambdaHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
@ -63,6 +64,12 @@ class ReflectionHintsAttributes { @@ -63,6 +64,12 @@ class ReflectionHintsAttributes {
return leftSignature.compareTo(rightSignature);
};
private static final Comparator<LambdaHint> LAMBDA_HINT_COMPARATOR =
Comparator.comparing(LambdaHint::getDeclaringClass)
.thenComparing(LambdaHint::getDeclaringMethod, Comparator.nullsFirst(
Comparator.comparing(LambdaHint.DeclaringMethod::name)));
public List<Map<String, Object>> reflection(RuntimeHints hints) {
List<Map<String, Object>> reflectionHints = new ArrayList<>(reflectionHints(hints));
reflectionHints.addAll(hints.proxies().jdkProxyHints()
@ -83,7 +90,9 @@ class ReflectionHintsAttributes { @@ -83,7 +90,9 @@ class ReflectionHintsAttributes {
return currentAttributes;
});
});
return allTypeHints.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).map(Map.Entry::getValue).toList();
return Stream.concat(
allTypeHints.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue),
hints.reflection().lambdaHints().sorted(LAMBDA_HINT_COMPARATOR).map(this::toAttributes)).toList();
}
public List<Map<String, Object>> jni(RuntimeHints hints) {
@ -106,6 +115,23 @@ class ReflectionHintsAttributes { @@ -106,6 +115,23 @@ class ReflectionHintsAttributes {
return attributes;
}
private Map<String, Object> toAttributes(LambdaHint hint) {
Map<String, Object> attributes = new LinkedHashMap<>();
Map<String, Object> lambdaAttributes = new LinkedHashMap<>();
lambdaAttributes.put("declaringClass", hint.getDeclaringClass());
LambdaHint.DeclaringMethod declaringMethod = hint.getDeclaringMethod();
if (declaringMethod != null) {
Map<String, Object> methodAttributes = new LinkedHashMap<>();
methodAttributes.put("name", declaringMethod.name());
methodAttributes.put("parameterTypes", declaringMethod.parameterTypes());
lambdaAttributes.put("declaringMethod", methodAttributes);
}
lambdaAttributes.put("interfaces", hint.getInterfaces());
attributes.put("lambda", lambdaAttributes);
return Map.of("type", attributes);
}
@SuppressWarnings("removal")
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();

26
spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java

@ -20,7 +20,9 @@ import java.io.Serializable; @@ -20,7 +20,9 @@ import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@ -223,6 +225,30 @@ class ReflectionHintsTests { @@ -223,6 +225,30 @@ class ReflectionHintsTests {
typeHint.getMemberCategories().contains(MemberCategory.INTROSPECT_PUBLIC_METHODS));
}
@Test
void registerLambda() {
this.reflectionHints.registerLambda(String.class, lambdaHint -> lambdaHint.withInterfaces(Supplier.class));
assertThat(this.reflectionHints.lambdaHints()).singleElement().satisfies(lambdaHint -> {
assertThat(lambdaHint.getDeclaringClass()).isEqualTo(TypeReference.of(String.class));
assertThat(lambdaHint.getReachableType()).isNull();
assertThat(lambdaHint.getDeclaringMethod()).isNull();
assertThat(lambdaHint.getInterfaces()).containsExactly(TypeReference.of(Supplier.class));
});
}
@Test
void registerLambdaWithDeclaringMethod() {
this.reflectionHints.registerLambda(TypeReference.of("com.example.Demo"), lambdaHint -> lambdaHint.withInterfaces(Supplier.class)
.withDeclaringMethod("hello", String.class, Integer.class));
assertThat(this.reflectionHints.lambdaHints()).singleElement().satisfies(lambdaHint -> {
assertThat(lambdaHint.getDeclaringClass()).isEqualTo(TypeReference.of("com.example.Demo"));
assertThat(lambdaHint.getReachableType()).isNull();
assertThat(lambdaHint.getDeclaringMethod()).isEqualTo(new LambdaHint.DeclaringMethod("hello",
List.of(TypeReference.of(String.class), TypeReference.of(Integer.class))));
assertThat(lambdaHint.getInterfaces()).containsExactly(TypeReference.of(Supplier.class));
});
}
private void assertTestTypeMethodHints(Consumer<ExecutableHint> methodHint) {
assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName());

69
spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java

@ -21,6 +21,7 @@ import java.nio.charset.Charset; @@ -21,6 +21,7 @@ import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@ -293,6 +294,74 @@ class RuntimeHintsWriterTests { @@ -293,6 +294,74 @@ class RuntimeHintsWriterTests {
assertThat(writeJson(hints)).isEqualTo(writeJson(hints2));
}
@Test
void oneLambda() throws JSONException {
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerLambda(Integer.class, builder -> builder.withDeclaringMethod("getCell", Integer.class, Integer.class).withInterfaces(Supplier.class));
assertEquals("""
{
"reflection": [
{
"type": {
"lambda": {
"declaringClass": "java.lang.Integer",
"declaringMethod": {
"name": "getCell",
"parameterTypes": [ "java.lang.Integer", "java.lang.Integer" ]
},
"interfaces": [ "java.util.function.Supplier" ]
}
}
}
]
}
""", hints);
}
@Test
void sortLambdasByDeclaringClassAndMethods() throws JSONException {
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerLambda(String.class, builder -> builder.withInterfaces(Callable.class));
hints.reflection().registerLambda(Integer.class,
builder -> builder.withDeclaringMethod("def").withInterfaces(Supplier.class));
hints.reflection().registerLambda(Integer.class,
builder -> builder.withDeclaringMethod("abc").withInterfaces(Function.class));
assertEquals("""
{
"reflection": [
{
"type": {
"lambda": {
"declaringClass": "java.lang.Integer",
"declaringMethod": { "name": "abc" },
"interfaces": [ "java.util.function.Function" ]
}
}
},
{
"type": {
"lambda": {
"declaringClass": "java.lang.Integer",
"declaringMethod": { "name": "def" },
"interfaces": [ "java.util.function.Supplier" ]
}
}
},
{
"type": {
"lambda": {
"declaringClass": "java.lang.String",
"interfaces": [ "java.util.concurrent.Callable" ]
}
}
}
]
}
""", hints);
}
}

Loading…
Cancel
Save