From 03d5c32ef4ebb1810480abcc51deda9cf91c32a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 1 May 2025 09:58:31 +0100 Subject: [PATCH] Register for reflection fields backing JavaBean properties Previously, the fields that back JavaBean properties were not registered for reflection. In a native image, this meant that the binding process did not find any annotations such as `@DataSizeUnit` and `@DurationUnit` so any custom default unit was ignored. Fixes gh-45343 --- .../bind/BindableRuntimeHintsRegistrar.java | 6 +- .../properties/bind/JavaBeanBinder.java | 6 +- .../BindableRuntimeHintsRegistrarTests.java | 118 ++++++++++++------ 3 files changed, 91 insertions(+), 39 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java index 6f70266024..3516ac12ae 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -221,6 +221,10 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar { if (setter != null) { hints.registerMethod(setter, ExecutableMode.INVOKE); } + Field field = property.getField(); + if (field != null) { + hints.registerField(field); + } handleProperty(hints, name, property.getType()); }); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 44af313086..16a11088d1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -419,6 +419,10 @@ class JavaBeanBinder implements DataObjectBinder { return this.setter; } + Field getField() { + return this.field; + } + } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrarTests.java index 6b87bca436..7c29c78c3a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -28,6 +28,7 @@ import org.apache.tomcat.jdbc.pool.PoolProperties; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.ExecutableHint; +import org.springframework.aot.hint.FieldHint; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; @@ -108,15 +109,15 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenJavaBeanWithSeveralConstructors() throws NoSuchMethodException { RuntimeHints runtimeHints = registerHints(WithSeveralConstructors.class); assertThat(runtimeHints.reflection().typeHints()).singleElement() - .satisfies(javaBeanBinding(WithSeveralConstructors.class, - WithSeveralConstructors.class.getDeclaredConstructor())); + .satisfies(javaBeanBinding(WithSeveralConstructors.class) + .constructor(WithSeveralConstructors.class.getDeclaredConstructor())); } @Test void registerHintsWhenJavaBeanWithMapOfPojo() { RuntimeHints runtimeHints = registerHints(WithMap.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(2) - .anySatisfy(javaBeanBinding(WithMap.class, "getAddresses")) + .anySatisfy(javaBeanBinding(WithMap.class).methods("getAddresses")) .anySatisfy(javaBeanBinding(Address.class)); } @@ -124,7 +125,7 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenJavaBeanWithListOfPojo() { RuntimeHints runtimeHints = registerHints(WithList.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(2) - .anySatisfy(javaBeanBinding(WithList.class, "getAllAddresses")) + .anySatisfy(javaBeanBinding(WithList.class).methods("getAllAddresses")) .anySatisfy(javaBeanBinding(Address.class)); } @@ -132,7 +133,7 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenJavaBeanWitArrayOfPojo() { RuntimeHints runtimeHints = registerHints(WithArray.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(2) - .anySatisfy(javaBeanBinding(WithArray.class, "getAllAddresses")) + .anySatisfy(javaBeanBinding(WithArray.class).methods("getAllAddresses")) .anySatisfy(javaBeanBinding(Address.class)); } @@ -140,7 +141,7 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenJavaBeanWithListOfJavaType() { RuntimeHints runtimeHints = registerHints(WithSimpleList.class); assertThat(runtimeHints.reflection().typeHints()).singleElement() - .satisfies(javaBeanBinding(WithSimpleList.class, "getNames")); + .satisfies(javaBeanBinding(WithSimpleList.class).methods("getNames")); } @Test @@ -177,9 +178,10 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenWhenHasNestedExternalType() { RuntimeHints runtimeHints = registerHints(WithExternalNested.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(3) - .anySatisfy( - javaBeanBinding(WithExternalNested.class, "getName", "setName", "getSampleType", "setSampleType")) - .anySatisfy(javaBeanBinding(SampleType.class, "getNested")) + .anySatisfy(javaBeanBinding(WithExternalNested.class) + .methods("getName", "setName", "getSampleType", "setSampleType") + .fields("name", "sampleType")) + .anySatisfy(javaBeanBinding(SampleType.class).methods("getNested").fields("nested")) .anySatisfy(javaBeanBinding(SampleType.Nested.class)); } @@ -187,8 +189,9 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenHasRecursiveType() { RuntimeHints runtimeHints = registerHints(WithRecursive.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(2) - .anySatisfy(javaBeanBinding(WithRecursive.class, "getRecursive", "setRecursive")) - .anySatisfy(javaBeanBinding(Recursive.class, "getRecursive", "setRecursive")); + .anySatisfy( + javaBeanBinding(WithRecursive.class).methods("getRecursive", "setRecursive").fields("recursive")) + .anySatisfy(javaBeanBinding(Recursive.class).methods("getRecursive", "setRecursive").fields("recursive")); } @Test @@ -203,24 +206,28 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenHasWellKnownTypes() { RuntimeHints runtimeHints = registerHints(WithWellKnownTypes.class); assertThat(runtimeHints.reflection().typeHints()).singleElement() - .satisfies(javaBeanBinding(WithWellKnownTypes.class, "getApplicationContext", "setApplicationContext", - "getEnvironment", "setEnvironment")); + .satisfies(javaBeanBinding(WithWellKnownTypes.class) + .methods("getApplicationContext", "setApplicationContext", "getEnvironment", "setEnvironment") + .fields("applicationContext", "environment")); } @Test void registerHintsWhenHasCrossReference() { RuntimeHints runtimeHints = registerHints(WithCrossReference.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(3) - .anySatisfy(javaBeanBinding(WithCrossReference.class, "getCrossReferenceA", "setCrossReferenceA")) - .anySatisfy(javaBeanBinding(CrossReferenceA.class, "getCrossReferenceB", "setCrossReferenceB")) - .anySatisfy(javaBeanBinding(CrossReferenceB.class, "getCrossReferenceA", "setCrossReferenceA")); + .anySatisfy(javaBeanBinding(WithCrossReference.class).methods("getCrossReferenceA", "setCrossReferenceA") + .fields("crossReferenceA")) + .anySatisfy(javaBeanBinding(CrossReferenceA.class).methods("getCrossReferenceB", "setCrossReferenceB") + .fields("crossReferenceB")) + .anySatisfy(javaBeanBinding(CrossReferenceB.class).methods("getCrossReferenceA", "setCrossReferenceA") + .fields("crossReferenceA")); } @Test void registerHintsWhenHasUnresolvedGeneric() { RuntimeHints runtimeHints = registerHints(WithGeneric.class); assertThat(runtimeHints.reflection().typeHints()).hasSize(2) - .anySatisfy(javaBeanBinding(WithGeneric.class, "getGeneric")) + .anySatisfy(javaBeanBinding(WithGeneric.class).methods("getGeneric").fields("generic")) .anySatisfy(javaBeanBinding(GenericObject.class)); } @@ -246,8 +253,9 @@ class BindableRuntimeHintsRegistrarTests { void registerHintsWhenHasPackagePrivateGettersAndSetters() { RuntimeHints runtimeHints = registerHints(PackagePrivateGettersAndSetters.class); assertThat(runtimeHints.reflection().typeHints()).singleElement() - .satisfies(javaBeanBinding(PackagePrivateGettersAndSetters.class, "getAlpha", "setAlpha", "getBravo", - "setBravo")); + .satisfies(javaBeanBinding(PackagePrivateGettersAndSetters.class) + .methods("getAlpha", "setAlpha", "getBravo", "setBravo") + .fields("alpha", "bravo")); } @Test @@ -260,9 +268,9 @@ class BindableRuntimeHintsRegistrarTests { .containsExactlyInAnyOrder("getInheritedNested", "setInheritedNested"); }); assertThat(runtimeHints.reflection().getTypeHint(ExtendingProperties.class)) - .satisfies(javaBeanBinding(ExtendingProperties.class, "getBravo", "setBravo")); + .satisfies(javaBeanBinding(ExtendingProperties.class).methods("getBravo", "setBravo").fields("bravo")); assertThat(runtimeHints.reflection().getTypeHint(InheritedNested.class)) - .satisfies(javaBeanBinding(InheritedNested.class, "getAlpha", "setAlpha")); + .satisfies(javaBeanBinding(InheritedNested.class).methods("getAlpha", "setAlpha").fields("alpha")); } @Test @@ -275,11 +283,11 @@ class BindableRuntimeHintsRegistrarTests { .containsExactlyInAnyOrder("getCount", "setCount"); }); assertThat(runtimeHints.reflection().getTypeHint(ListenerRetry.class)) - .satisfies(javaBeanBinding(ListenerRetry.class, "isStateless", "setStateless")); + .satisfies(javaBeanBinding(ListenerRetry.class).methods("isStateless", "setStateless").fields("stateless")); assertThat(runtimeHints.reflection().getTypeHint(Simple.class)) - .satisfies(javaBeanBinding(Simple.class, "getRetry")); + .satisfies(javaBeanBinding(Simple.class).methods("getRetry").fields("retry")); assertThat(runtimeHints.reflection().getTypeHint(ComplexNestedProperties.class)) - .satisfies(javaBeanBinding(ComplexNestedProperties.class, "getSimple")); + .satisfies(javaBeanBinding(ComplexNestedProperties.class).methods("getSimple").fields("simple")); } @Test @@ -292,17 +300,8 @@ class BindableRuntimeHintsRegistrarTests { assertThatNoException().isThrownBy(() -> registerHints(PoolProperties.class)); } - private Consumer javaBeanBinding(Class type, String... expectedMethods) { - return javaBeanBinding(type, type.getDeclaredConstructors()[0], expectedMethods); - } - - private Consumer javaBeanBinding(Class type, Constructor constructor, String... expectedMethods) { - return (entry) -> { - assertThat(entry.getType()).isEqualTo(TypeReference.of(type)); - assertThat(entry.constructors()).singleElement().satisfies(match(constructor)); - assertThat(entry.getMemberCategories()).isEmpty(); - assertThat(entry.methods()).extracting(ExecutableHint::getName).containsExactlyInAnyOrder(expectedMethods); - }; + private JavaBeanBinding javaBeanBinding(Class type) { + return new JavaBeanBinding(type); } private Consumer valueObjectBinding(Class type) { @@ -318,7 +317,7 @@ class BindableRuntimeHintsRegistrarTests { }; } - private Consumer match(Constructor constructor) { + private static Consumer match(Constructor constructor) { return (executableHint) -> { assertThat(executableHint.getName()).isEqualTo(""); assertThat(Arrays.stream(constructor.getParameterTypes()).map(TypeReference::of).toList()) @@ -804,4 +803,49 @@ class BindableRuntimeHintsRegistrarTests { } + private static final class JavaBeanBinding implements Consumer { + + private final Class type; + + private Constructor constructor; + + private List expectedMethods = Collections.emptyList(); + + private List expectedFields = Collections.emptyList(); + + private JavaBeanBinding(Class type) { + this.type = type; + this.constructor = this.type.getDeclaredConstructors()[0]; + } + + @Override + public void accept(TypeHint entry) { + assertThat(entry.getType()).isEqualTo(TypeReference.of(this.type)); + assertThat(entry.constructors()).singleElement().satisfies(match(this.constructor)); + assertThat(entry.getMemberCategories()).isEmpty(); + assertThat(entry.methods()).as("Methods requiring reflection") + .extracting(ExecutableHint::getName) + .containsExactlyInAnyOrderElementsOf(this.expectedMethods); + assertThat(entry.fields()).as("Fields requiring reflection") + .extracting(FieldHint::getName) + .containsExactlyInAnyOrderElementsOf(this.expectedFields); + } + + private JavaBeanBinding constructor(Constructor constructor) { + this.constructor = constructor; + return this; + } + + private JavaBeanBinding methods(String... methods) { + this.expectedMethods = List.of(methods); + return this; + } + + private JavaBeanBinding fields(String... fields) { + this.expectedFields = List.of(fields); + return this; + } + + } + }