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 | 39 ++++++- .../SimpleReflectiveProcessorTests.java | 4 +- .../ReflectionHintsPredicatesTests.java | 21 ++-- .../FileNativeConfigurationWriterTests.java | 2 +- .../nativex/ReflectionHintsWriterTests.java | 6 +- .../MessageMappingReflectiveProcessor.java | 4 +- .../RequestMappingReflectiveProcessor.java | 4 +- 20 files changed, 273 insertions(+), 87 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 e00d3a7792c..78378f6b0a1 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 76330a04f58..d3d4b4de389 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 be7bc11d58c..c59c8fc01da 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 483638bf675..b6f09e3d118 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 db06fafd890..cd11fdc1b01 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 9d5599ad83b..7082859e750 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 d6976f20349..593d5b5588c 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 c5386cfdbb7..b9460ed01bf 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 4ad5e9363dd..6d4d87277a5 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 cd4c902a848..61b7dbd8456 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 bd222342108..d370570c9a8 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 00000000000..d19df791de2 --- /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 68458881dde..24799c99da8 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 e781352c523..f66b83120c4 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,7 +125,7 @@ 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); }); } @@ -120,13 +133,27 @@ class TypeHintTests { void createWithMethodReuseBuilder() { List parameterTypes = TypeReference.listOf(char[].class); Builder builder = TypeHint.of(TypeReference.of(String.class)).withMethod("valueOf", parameterTypes, - methodHint -> methodHint.withMode(ExecutableMode.INVOKE)); + methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT)); TypeHint hint = builder.withMethod("valueOf", parameterTypes, - methodHint -> methodHint.setModes(ExecutableMode.INTROSPECT)).build(); + methodHint -> methodHint.withMode(ExecutableMode.INVOKE)).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); + }); + } + + @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.withMode(ExecutableMode.INTROSPECT)).build(); + assertThat(hint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("valueOf"); + assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + 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 4ab2c603d88..45bf6f7c9d7 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 d3390fe15b4..3706197d509 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 dc77407689a..121e75c9ebf 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 9060f5b0afa..e3c547a79b2 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 f1004dc6ba3..4f7ac3942f3 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 c1661d39f63..d61868aeb0e 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) ||