diff --git a/spring-core/src/main/java/org/springframework/aot/hint/LambdaHint.java b/spring-core/src/main/java/org/springframework/aot/hint/LambdaHint.java new file mode 100644 index 00000000000..ae9f8018986 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/LambdaHint.java @@ -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 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 getInterfaces() { + return this.interfaces; + } + + public static class Builder { + + private final TypeReference declaringClass; + + private @Nullable TypeReference reachableType; + + private @Nullable DeclaringMethod declaringMethod; + + private final List 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 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 parameterTypes) { + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java index a5020d13a0b..48869c9b83d 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java @@ -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 { private final Map types = new HashMap<>(); + private final Set lambdaHints = new LinkedHashSet<>(); /** * Return the types that require reflection. @@ -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 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 { 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) { + 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) { + return this.registerLambda(TypeReference.of(declaringClass), lambdaHint); + } + private List mapParameters(Executable executable) { return TypeReference.listOf(executable.getParameterTypes()); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java index ea6ddd1b33f..01f2ba37672 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java @@ -19,10 +19,10 @@ package org.springframework.aot.hint; /** * Gather hints that can be used to optimize the application runtime. * - *

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. + *

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. * *

Hints that require the need for Java serialization of proxies can be * recorded as well. diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java index aea5594676b..ba93dd97d81 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java @@ -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 { return leftSignature.compareTo(rightSignature); }; + private static final Comparator LAMBDA_HINT_COMPARATOR = + Comparator.comparing(LambdaHint::getDeclaringClass) + .thenComparing(LambdaHint::getDeclaringMethod, Comparator.nullsFirst( + Comparator.comparing(LambdaHint.DeclaringMethod::name))); + + public List> reflection(RuntimeHints hints) { List> reflectionHints = new ArrayList<>(reflectionHints(hints)); reflectionHints.addAll(hints.proxies().jdkProxyHints() @@ -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> jni(RuntimeHints hints) { @@ -106,6 +115,23 @@ class ReflectionHintsAttributes { return attributes; } + private Map toAttributes(LambdaHint hint) { + Map attributes = new LinkedHashMap<>(); + Map lambdaAttributes = new LinkedHashMap<>(); + lambdaAttributes.put("declaringClass", hint.getDeclaringClass()); + LambdaHint.DeclaringMethod declaringMethod = hint.getDeclaringMethod(); + if (declaringMethod != null) { + Map 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 toAttributes(JavaSerializationHint serializationHint) { LinkedHashMap attributes = new LinkedHashMap<>(); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java index 570bab0b7c6..cab729e60cd 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java @@ -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 { 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 methodHint) { assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java index aaa63ed5262..75f83d2c318 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java @@ -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 { 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); + } + }