From c164b918c0f2900e3bb3e9662432235a45f5a68b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 24 Aug 2022 15:12:46 +0200 Subject: [PATCH] Apply consistent RuntimeHints defaults This commit harmonizes the registration of an executable so that the default method and the method that takes an empty customizer produces the same hint. The same applies to the readable flag of a field hint. Rather than returning a list of executable modes, the "highest" mode is retained. See gh-29011 --- .../InstanceSupplierCodeGeneratorTests.java | 2 +- .../BindingReflectionHintsRegistrarTests.java | 12 +- ...linBindingReflectionHintsRegistrarTests.kt | 5 +- .../aot/agent/InstrumentedMethodTests.java | 12 +- .../aot/hint/ExecutableHint.java | 36 +++--- .../aot/hint/ExecutableMode.java | 11 ++ .../springframework/aot/hint/FieldHint.java | 7 +- .../aot/hint/ReflectionHints.java | 3 +- .../annotation/SimpleReflectiveProcessor.java | 9 +- .../predicate/ReflectionHintsPredicates.java | 10 +- .../aot/nativex/ReflectionHintsWriter.java | 4 +- .../aot/hint/ExecutableModeTests.java | 60 ++++++++++ .../aot/hint/ReflectionHintsTests.java | 109 ++++++++++++++++-- .../aot/hint/TypeHintTests.java | 37 +++++- .../SimpleReflectiveProcessorTests.java | 4 +- .../ReflectionHintsPredicatesTests.java | 21 ++-- .../FileNativeConfigurationWriterTests.java | 2 +- .../nativex/ReflectionHintsWriterTests.java | 6 +- .../MessageMappingReflectiveProcessor.java | 4 +- .../RequestMappingReflectiveProcessor.java | 4 +- 20 files changed, 272 insertions(+), 86 deletions(-) create mode 100644 spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java index e00d3a7792..78378f6b0a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java @@ -283,7 +283,7 @@ class InstanceSupplierCodeGeneratorTests { } private ThrowingConsumer hasMode(ExecutableMode mode) { - return hint -> assertThat(hint.getModes()).containsExactly(mode); + return hint -> assertThat(hint.getMode()).isEqualTo(mode); } @SuppressWarnings("unchecked") diff --git a/spring-context/src/test/java/org/springframework/context/aot/BindingReflectionHintsRegistrarTests.java b/spring-context/src/test/java/org/springframework/context/aot/BindingReflectionHintsRegistrarTests.java index 76330a04f5..d3d4b4de38 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/BindingReflectionHintsRegistrarTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/BindingReflectionHintsRegistrarTests.java @@ -77,7 +77,7 @@ public class BindingReflectionHintsRegistrarTests { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithGetter.class)); assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("getName"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); }); } @@ -97,7 +97,7 @@ public class BindingReflectionHintsRegistrarTests { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithSetter.class)); assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("setName"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); }); } @@ -125,11 +125,11 @@ public class BindingReflectionHintsRegistrarTests { assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder( methodHint -> { assertThat(methodHint.getName()).isEqualTo("setNames"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }, methodHint -> { assertThat(methodHint.getName()).isEqualTo("getNames"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); }); } @@ -180,7 +180,7 @@ public class BindingReflectionHintsRegistrarTests { assertThat(typeHint.methods()).singleElement().satisfies( methodHint -> { assertThat(methodHint.getName()).isEqualTo("getResolvableType"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); }); } @@ -218,7 +218,7 @@ public class BindingReflectionHintsRegistrarTests { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleRecord.class)); assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("name"); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); }); } diff --git a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinBindingReflectionHintsRegistrarTests.kt b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinBindingReflectionHintsRegistrarTests.kt index be7bc11d58..c59c8fc01d 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinBindingReflectionHintsRegistrarTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinBindingReflectionHintsRegistrarTests.kt @@ -48,8 +48,7 @@ class KotlinBindingReflectionHintsRegistrarTests { assertThat(typeHint.methods()).singleElement() .satisfies(ThrowingConsumer { methodHint: ExecutableHint -> assertThat(methodHint.name).isEqualTo("getName") - assertThat(methodHint.modes) - .containsOnly(ExecutableMode.INVOKE) + assertThat(methodHint.mode).isEqualTo(ExecutableMode.INVOKE) }) }, ThrowingConsumer { typeHint: TypeHint -> @@ -57,7 +56,7 @@ class KotlinBindingReflectionHintsRegistrarTests { assertThat(typeHint.methods()).singleElement() .satisfies(ThrowingConsumer { methodHint: ExecutableHint -> assertThat(methodHint.name).isEqualTo("serializer") - assertThat(methodHint.modes).containsOnly(ExecutableMode.INVOKE) + assertThat(methodHint.mode).isEqualTo(ExecutableMode.INVOKE) }) }) } diff --git a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java index 483638bf67..b6f09e3d11 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java @@ -152,14 +152,14 @@ class InstrumentedMethodTests { @Test void classGetConstructorShouldMatchInstrospectConstructorHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), - constructorHint -> constructorHint.setModes(ExecutableMode.INTROSPECT))); + constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @Test void classGetConstructorShouldMatchInvokeConstructorHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), - constructorHint -> constructorHint.setModes(ExecutableMode.INVOKE))); + constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @@ -202,14 +202,14 @@ class InstrumentedMethodTests { @Test void classGetDeclaredConstructorShouldMatchInstrospectConstructorHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(TypeReference.listOf(byte[].class, byte.class), - constructorHint -> constructorHint.setModes(ExecutableMode.INTROSPECT))); + constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); } @Test void classGetDeclaredConstructorShouldMatchInvokeConstructorHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(TypeReference.listOf(byte[].class, byte.class), - constructorHint -> constructorHint.setModes(ExecutableMode.INVOKE))); + constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); } @@ -378,14 +378,14 @@ class InstrumentedMethodTests { @Test void classGetMethodShouldMatchIntrospectMethodHint() { hints.reflection().registerType(String.class, typeHint -> - typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.setModes(ExecutableMode.INTROSPECT))); + typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @Test void classGetMethodShouldMatchInvokeMethodHint() { hints.reflection().registerType(String.class, typeHint -> - typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.setModes(ExecutableMode.INVOKE))); + typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java index db06fafd89..cd11fdc1b0 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java @@ -19,12 +19,9 @@ package org.springframework.aot.hint; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; -import org.springframework.util.ObjectUtils; +import org.springframework.lang.Nullable; /** * A hint that describes the need for reflection on a {@link Method} or @@ -37,13 +34,13 @@ public final class ExecutableHint extends MemberHint { private final List parameterTypes; - private final List modes; + private final ExecutableMode mode; private ExecutableHint(Builder builder) { super(builder.name); this.parameterTypes = List.copyOf(builder.parameterTypes); - this.modes = List.copyOf(builder.modes); + this.mode = (builder.mode != null ? builder.mode : ExecutableMode.INVOKE); } /** @@ -75,11 +72,11 @@ public final class ExecutableHint extends MemberHint { } /** - * Return the {@linkplain ExecutableMode modes} that apply to this hint. + * Return the {@linkplain ExecutableMode mode} that apply to this hint. * @return the modes */ - public List getModes() { - return this.modes; + public ExecutableMode getMode() { + return this.mode; } @@ -92,7 +89,8 @@ public final class ExecutableHint extends MemberHint { private final List parameterTypes; - private final Set modes = new LinkedHashSet<>(); + @Nullable + private ExecutableMode mode; Builder(String name, List parameterTypes) { @@ -101,12 +99,14 @@ public final class ExecutableHint extends MemberHint { } /** - * Add the specified {@linkplain ExecutableMode mode} if necessary. - * @param mode the mode to add + * Specify that the {@linkplain ExecutableMode mode} is required. + * @param mode the required mode * @return {@code this}, to facilitate method chaining */ public Builder withMode(ExecutableMode mode) { - this.modes.add(mode); + if (this.mode == null || !this.mode.includes(mode)) { + this.mode = mode; + } return this; } @@ -114,11 +114,15 @@ public final class ExecutableHint extends MemberHint { * Set the {@linkplain ExecutableMode modes} to use. * @param modes the mode to use * @return {@code this}, to facilitate method chaining + * @deprecated only a single mode can be set, use {@link #withMode(ExecutableMode)} instead */ + @Deprecated public Builder setModes(ExecutableMode... modes) { - this.modes.clear(); - if (!ObjectUtils.isEmpty(modes)) { - this.modes.addAll(Arrays.asList(modes)); + if (modes.length > 1) { + throw new UnsupportedOperationException(); + } + if (modes.length == 1) { + withMode(modes[0]); } return this; } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java index 9d5599ad83..7082859e75 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java @@ -18,6 +18,8 @@ package org.springframework.aot.hint; import java.lang.reflect.Executable; +import org.springframework.lang.Nullable; + /** * Represent the need of reflection for a given {@link Executable}. * @@ -37,4 +39,13 @@ public enum ExecutableMode { */ INVOKE; + /** + * Specify if this mode already includes the specified {@code other} mode. + * @param other the other mode to check + * @return {@code true} if this mode includes the other mode + */ + boolean includes(@Nullable ExecutableMode other) { + return (other == null || this.ordinal() >= other.ordinal()); + } + } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/FieldHint.java b/spring-core/src/main/java/org/springframework/aot/hint/FieldHint.java index d6976f2034..593d5b5588 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/FieldHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/FieldHint.java @@ -18,6 +18,8 @@ package org.springframework.aot.hint; import java.lang.reflect.Field; +import org.springframework.lang.Nullable; + /** * A hint that describes the need of reflection on a {@link Field}. * @@ -33,7 +35,7 @@ public final class FieldHint extends MemberHint { private FieldHint(Builder builder) { super(builder.name); - this.allowWrite = builder.allowWrite; + this.allowWrite = (builder.allowWrite != null) ? builder.allowWrite : true; this.allowUnsafeAccess = builder.allowUnsafeAccess; } @@ -61,7 +63,8 @@ public final class FieldHint extends MemberHint { private final String name; - private boolean allowWrite; + @Nullable + private Boolean allowWrite; private boolean allowUnsafeAccess; 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 c5386cfdbb..b9460ed01b 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 @@ -165,8 +165,7 @@ public class ReflectionHints { * @return {@code this}, to facilitate method chaining */ public ReflectionHints registerConstructor(Constructor constructor) { - return registerConstructor(constructor, constructorHint -> - constructorHint.withMode(ExecutableMode.INVOKE)); + return registerConstructor(constructor, constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE)); } /** diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java index 4ad5e9363d..6d4d87277a 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessor.java @@ -20,10 +20,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.function.Consumer; -import org.springframework.aot.hint.ExecutableHint.Builder; -import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.ReflectionHints; /** @@ -36,8 +33,6 @@ import org.springframework.aot.hint.ReflectionHints; */ public class SimpleReflectiveProcessor implements ReflectiveProcessor { - private static final Consumer INVOKE_EXECUTABLE = hint -> hint.setModes(ExecutableMode.INVOKE); - @Override public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { if (element instanceof Class type) { @@ -69,7 +64,7 @@ public class SimpleReflectiveProcessor implements ReflectiveProcessor { * @param constructor the constructor to process */ protected void registerConstructorHint(ReflectionHints hints, Constructor constructor) { - hints.registerConstructor(constructor, INVOKE_EXECUTABLE); + hints.registerConstructor(constructor); } /** @@ -87,7 +82,7 @@ public class SimpleReflectiveProcessor implements ReflectiveProcessor { * @param method the method to process */ protected void registerMethodHint(ReflectionHints hints, Method method) { - hints.registerMethod(method, INVOKE_EXECUTABLE); + hints.registerMethod(method); } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java index cd4c902a84..61b7dbd845 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java @@ -263,11 +263,11 @@ public class ReflectionHintsPredicates { * and the configured {@code ExecutableMode} is compatibe */ static boolean includes(ExecutableHint hint, String name, - List parameterTypes, List executableModes) { + List parameterTypes, ExecutableMode executableModes) { return hint.getName().equals(name) && hint.getParameterTypes().equals(parameterTypes) - && (hint.getModes().contains(ExecutableMode.INVOKE) - || !executableModes.contains(ExecutableMode.INVOKE)); + && (hint.getMode().equals(ExecutableMode.INVOKE) + || !executableModes.equals(ExecutableMode.INVOKE)); } } @@ -302,7 +302,7 @@ public class ReflectionHintsPredicates { List parameters = Arrays.stream(this.executable.getParameterTypes()) .map(TypeReference::of).toList(); return includes(executableHint, "", - parameters, List.of(this.executableMode)); + parameters, this.executableMode); }); } @@ -341,7 +341,7 @@ public class ReflectionHintsPredicates { List parameters = Arrays.stream(this.executable.getParameterTypes()) .map(TypeReference::of).toList(); return includes(executableHint, this.executable.getName(), - parameters, List.of(this.executableMode)); + parameters, this.executableMode); }); } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java index bd22234210..d370570c9a 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java @@ -85,10 +85,10 @@ class ReflectionHintsWriter { private void handleExecutables(Map attributes, List hints) { addIfNotEmpty(attributes, "methods", hints.stream() - .filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty()) + .filter(h -> h.getMode().equals(ExecutableMode.INVOKE)) .map(this::toAttributes).toList()); addIfNotEmpty(attributes, "queriedMethods", hints.stream() - .filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT)) + .filter(h -> h.getMode().equals(ExecutableMode.INTROSPECT)) .map(this::toAttributes).toList()); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java new file mode 100644 index 0000000000..d19df791de --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2022 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExecutableMode}. + * + * @author Stephane Nicoll + */ +class ExecutableModeTests { + + @Test + void invokeIncludesNullMode() { + assertThat(ExecutableMode.INVOKE.includes(null)).isTrue(); + } + + @Test + void invokeIncludesIntrospect() { + assertThat(ExecutableMode.INVOKE.includes(ExecutableMode.INTROSPECT)).isTrue(); + } + + @Test + void invokeIncludesIncludes() { + assertThat(ExecutableMode.INVOKE.includes(ExecutableMode.INVOKE)).isTrue(); + } + + @Test + void introspectIncludesNullMode() { + assertThat(ExecutableMode.INTROSPECT.includes(null)).isTrue(); + } + + @Test + void introspectIncludesIntrospect() { + assertThat(ExecutableMode.INTROSPECT.includes(ExecutableMode.INTROSPECT)).isTrue(); + } + + @Test + void introspectDoesNotIncludeInvoke() { + assertThat(ExecutableMode.INTROSPECT.includes(ExecutableMode.INVOKE)).isFalse(); + } + +} 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 68458881dd..24799c99da 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 @@ -121,14 +121,46 @@ class ReflectionHintsTests { } @Test - void registerField() { + void registerFieldAllowsWriteByDefault() { Field field = ReflectionUtils.findField(TestType.class, "field"); assertThat(field).isNotNull(); this.reflectionHints.registerField(field); + assertTestTypeFieldHint(fieldHint -> { + assertThat(fieldHint.getName()).isEqualTo("field"); + assertThat(fieldHint.isAllowWrite()).isTrue(); + assertThat(fieldHint.isAllowUnsafeAccess()).isFalse(); + }); + } + + @Test + void registerFieldWithEmptyCustomizerAppliesConsistentDefault() { + Field field = ReflectionUtils.findField(TestType.class, "field"); + assertThat(field).isNotNull(); + this.reflectionHints.registerField(field, fieldHint -> {}); + assertTestTypeFieldHint(fieldHint -> { + assertThat(fieldHint.getName()).isEqualTo("field"); + assertThat(fieldHint.isAllowWrite()).isTrue(); + assertThat(fieldHint.isAllowUnsafeAccess()).isFalse(); + }); + } + + @Test + void registerFieldWithCustomizerAppliesCustomization() { + Field field = ReflectionUtils.findField(TestType.class, "field"); + assertThat(field).isNotNull(); + this.reflectionHints.registerField(field, fieldHint -> + fieldHint.allowWrite(false).allowUnsafeAccess(true)); + assertTestTypeFieldHint(fieldHint -> { + assertThat(fieldHint.getName()).isEqualTo("field"); + assertThat(fieldHint.isAllowWrite()).isFalse(); + assertThat(fieldHint.isAllowUnsafeAccess()).isTrue(); + }); + } + + private void assertTestTypeFieldHint(Consumer fieldHint) { assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); - assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> - assertThat(fieldHint.getName()).isEqualTo("field")); + assertThat(typeHint.fields()).singleElement().satisfies(fieldHint); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.methods()).isEmpty(); assertThat(typeHint.getMemberCategories()).isEmpty(); @@ -138,14 +170,38 @@ class ReflectionHintsTests { @Test void registerConstructor() { this.reflectionHints.registerConstructor(TestType.class.getDeclaredConstructors()[0]); + assertTestTypeConstructorHint(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).isEmpty(); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void registerConstructorWithEmptyCustomizerAppliesConsistentDefault() { + this.reflectionHints.registerConstructor(TestType.class.getDeclaredConstructors()[0], + constructorHint -> {}); + assertTestTypeConstructorHint(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).isEmpty(); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void registerConstructorWithCustomizerAppliesCustomization() { + this.reflectionHints.registerConstructor(TestType.class.getDeclaredConstructors()[0], + constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT)); + assertTestTypeConstructorHint(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).isEmpty(); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INTROSPECT); + }); + } + + private void assertTestTypeConstructorHint(Consumer constructorHint) { assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { assertThat(typeHint.getMemberCategories()).isEmpty(); assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); assertThat(typeHint.fields()).isEmpty(); - assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint -> { - assertThat(constructorHint.getParameterTypes()).isEmpty(); - assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE); - }); + assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint); assertThat(typeHint.methods()).isEmpty(); assertThat(typeHint.getMemberCategories()).isEmpty(); }); @@ -156,15 +212,44 @@ class ReflectionHintsTests { Method method = ReflectionUtils.findMethod(TestType.class, "setName", String.class); assertThat(method).isNotNull(); this.reflectionHints.registerMethod(method); + assertTestTypeMethodHints(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("setName"); + assertThat(methodHint.getParameterTypes()).containsOnly(TypeReference.of(String.class)); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void registerMethodWithEmptyCustomizerAppliesConsistentDefault() { + Method method = ReflectionUtils.findMethod(TestType.class, "setName", String.class); + assertThat(method).isNotNull(); + this.reflectionHints.registerMethod(method, methodHint -> {}); + assertTestTypeMethodHints(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("setName"); + assertThat(methodHint.getParameterTypes()).containsOnly(TypeReference.of(String.class)); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void registerMethodWithCustomizerAppliesCustomization() { + Method method = ReflectionUtils.findMethod(TestType.class, "setName", String.class); + assertThat(method).isNotNull(); + this.reflectionHints.registerMethod(method, methodHint -> + methodHint.withMode(ExecutableMode.INTROSPECT)); + assertTestTypeMethodHints(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("setName"); + assertThat(methodHint.getParameterTypes()).containsOnly(TypeReference.of(String.class)); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INTROSPECT); + }); + } + + private void assertTestTypeMethodHints(Consumer methodHint) { assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.constructors()).isEmpty(); - assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { - assertThat(methodHint.getName()).isEqualTo("setName"); - assertThat(methodHint.getParameterTypes()).containsOnly(TypeReference.of(String.class)); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); - }); + assertThat(typeHint.methods()).singleElement().satisfies(methodHint); }); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java b/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java index e781352c52..f66b83120c 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java @@ -87,12 +87,25 @@ class TypeHintTests { constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE)).build(); assertThat(hint.constructors()).singleElement().satisfies(constructorHint -> { assertThat(constructorHint.getParameterTypes()).containsOnlyOnceElementsOf(parameterTypes); - assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); } @Test void createConstructorReuseBuilder() { + List parameterTypes = TypeReference.listOf(byte[].class, int.class); + Builder builder = TypeHint.of(TypeReference.of(String.class)).withConstructor(parameterTypes, + constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT)); + TypeHint hint = builder.withConstructor(parameterTypes, constructorHint -> + constructorHint.withMode(ExecutableMode.INVOKE)).build(); + assertThat(hint.constructors()).singleElement().satisfies(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void createConstructorReuseBuilderAndApplyExecutableModePrecedence() { List parameterTypes = TypeReference.listOf(byte[].class, int.class); Builder builder = TypeHint.of(TypeReference.of(String.class)).withConstructor(parameterTypes, constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE)); @@ -100,7 +113,7 @@ class TypeHintTests { constructorHint.withMode(ExecutableMode.INTROSPECT)).build(); assertThat(hint.constructors()).singleElement().satisfies(constructorHint -> { assertThat(constructorHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); - assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE, ExecutableMode.INTROSPECT); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); } @@ -112,21 +125,35 @@ class TypeHintTests { assertThat(hint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("valueOf"); assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); } @Test void createWithMethodReuseBuilder() { + List parameterTypes = TypeReference.listOf(char[].class); + Builder builder = TypeHint.of(TypeReference.of(String.class)).withMethod("valueOf", parameterTypes, + methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT)); + TypeHint hint = builder.withMethod("valueOf", parameterTypes, + methodHint -> methodHint.withMode(ExecutableMode.INVOKE)).build(); + assertThat(hint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("valueOf"); + assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); + }); + } + + @Test + void createWithMethodReuseBuilderAndApplyExecutableModePrecedence() { List parameterTypes = TypeReference.listOf(char[].class); Builder builder = TypeHint.of(TypeReference.of(String.class)).withMethod("valueOf", parameterTypes, methodHint -> methodHint.withMode(ExecutableMode.INVOKE)); TypeHint hint = builder.withMethod("valueOf", parameterTypes, - methodHint -> methodHint.setModes(ExecutableMode.INTROSPECT)).build(); + methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT)).build(); assertThat(hint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("valueOf"); assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); - assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INTROSPECT); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); }); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java index 4ab2c603d8..45bf6f7c9d 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/SimpleReflectiveProcessorTests.java @@ -60,7 +60,7 @@ class SimpleReflectiveProcessorTests { assertThat(typeHint.getMemberCategories()).isEmpty(); assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint -> { assertThat(constructorHint.getName()).isEqualTo(""); - assertThat(constructorHint.getModes()).containsExactly(ExecutableMode.INVOKE); + assertThat(constructorHint.getMode()).isEqualTo(ExecutableMode.INVOKE); assertThat(constructorHint.getParameterTypes()).containsExactly(TypeReference.of(String.class)); }); assertThat(typeHint.fields()).isEmpty(); @@ -93,7 +93,7 @@ class SimpleReflectiveProcessorTests { assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { assertThat(methodHint.getName()).isEqualTo("setName"); - assertThat(methodHint.getModes()).containsExactly(ExecutableMode.INVOKE); + assertThat(methodHint.getMode()).isEqualTo(ExecutableMode.INVOKE); assertThat(methodHint.getParameterTypes()).containsExactly(TypeReference.of(String.class)); }); }); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java index d3390fe15b..3706197d50 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java @@ -192,7 +192,8 @@ class ReflectionHintsPredicatesTests { @Test void constructorInvocationDoesNotMatchConstructorHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint. - withConstructor(Collections.emptyList(), constructorHint -> {})); + withConstructor(Collections.emptyList(), constructorHint -> + constructorHint.withMode(ExecutableMode.INTROSPECT))); assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); } @@ -270,7 +271,8 @@ class ReflectionHintsPredicatesTests { @Test void privateConstructorInvocationDoesNotMatchConstructorHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> - typeHint.withConstructor(TypeReference.listOf(String.class), constructorHint -> {})); + typeHint.withConstructor(TypeReference.listOf(String.class), constructorHint -> + constructorHint.withMode(ExecutableMode.INTROSPECT))); assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); } @@ -348,14 +350,15 @@ class ReflectionHintsPredicatesTests { @Test void methodInvocationDoesNotMatchMethodHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), methodHint -> { - })); + runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), + methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT))); assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); } @Test void methodInvocationMatchesMethodInvocationHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); + runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), + methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); } @@ -416,8 +419,8 @@ class ReflectionHintsPredicatesTests { @Test void privateMethodInvocationDoesNotMatchMethodHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("privateMethod", Collections.emptyList(), methodHint -> { - })); + runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("privateMethod", Collections.emptyList(), + methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT))); assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); } @@ -470,8 +473,8 @@ class ReflectionHintsPredicatesTests { @Test void fieldWriteReflectionDoesNotMatchFieldHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField", fieldHint -> { - })); + runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField", + fieldHint -> fieldHint.allowWrite(false))); assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "publicField").allowWrite()); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index dc77407689..121e75c9eb 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -106,7 +106,7 @@ public class FileNativeConfigurationWriterTests { MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) - .withField("DEFAULT_CHARSET", fieldBuilder -> {}) + .withField("DEFAULT_CHARSET", fieldBuilder -> fieldBuilder.allowWrite(false)) .withField("defaultCharset", fieldBuilder -> { fieldBuilder.allowWrite(true); fieldBuilder.allowUnsafeAccess(true); diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java index 9060f5b0af..e3c547a79b 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java @@ -58,7 +58,7 @@ public class ReflectionHintsWriterTests { MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) - .withField("DEFAULT_CHARSET", fieldBuilder -> {}) + .withField("DEFAULT_CHARSET", fieldBuilder -> fieldBuilder.allowWrite(false)) .withField("defaultCharset", fieldBuilder -> { fieldBuilder.allowWrite(true); fieldBuilder.allowUnsafeAccess(true); @@ -185,7 +185,7 @@ public class ReflectionHintsWriterTests { hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", TypeReference.listOf(String.class), b -> b.withMode(ExecutableMode.INVOKE))); hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), b -> b.withMode(ExecutableMode.INTROSPECT))); + TypeReference.listOf(String.class, int.class), b -> b.withMode(ExecutableMode.INTROSPECT))); assertEquals(""" [ @@ -194,7 +194,7 @@ public class ReflectionHintsWriterTests { "queriedMethods": [ { "name": "parseInt", - "parameterTypes": ["java.lang.String"] + "parameterTypes": ["java.lang.String", "int"] } ], "methods": [ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java index f1004dc6ba..4f7ac3942f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java @@ -64,13 +64,13 @@ class MessageMappingReflectiveProcessor implements ReflectiveProcessor { } protected void registerMethodHints(ReflectionHints hints, Method method) { - hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE)); + hints.registerMethod(method, hint -> hint.withMode(ExecutableMode.INVOKE)); registerParameterHints(hints, method); registerReturnValueHints(hints, method); } protected void registerParameterHints(ReflectionHints hints, Method method) { - hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE)); + hints.registerMethod(method, hint -> hint.withMode(ExecutableMode.INVOKE)); for (Parameter parameter : method.getParameters()) { MethodParameter methodParameter = MethodParameter.forParameter(parameter); if (Message.class.isAssignableFrom(methodParameter.getParameterType())) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappingReflectiveProcessor.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappingReflectiveProcessor.java index c1661d39f6..d61868aeb0 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappingReflectiveProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappingReflectiveProcessor.java @@ -63,13 +63,13 @@ class RequestMappingReflectiveProcessor implements ReflectiveProcessor { } protected void registerMethodHints(ReflectionHints hints, Method method) { - hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE)); + hints.registerMethod(method, hint -> hint.withMode(ExecutableMode.INVOKE)); registerParameterHints(hints, method); registerReturnValueHints(hints, method); } protected void registerParameterHints(ReflectionHints hints, Method method) { - hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE)); + hints.registerMethod(method, hint -> hint.withMode(ExecutableMode.INVOKE)); for (Parameter parameter : method.getParameters()) { MethodParameter methodParameter = MethodParameter.forParameter(parameter); if (methodParameter.hasParameterAnnotation(RequestBody.class) ||