Skip to content

Commit 03d5c32

Browse files
committed
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
1 parent 669909e commit 03d5c32

File tree

3 files changed

+91
-39
lines changed

3 files changed

+91
-39
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -221,6 +221,10 @@ private void handleJavaBeanProperties(ReflectionHints hints) {
221221
if (setter != null) {
222222
hints.registerMethod(setter, ExecutableMode.INVOKE);
223223
}
224+
Field field = property.getField();
225+
if (field != null) {
226+
hints.registerField(field);
227+
}
224228
handleProperty(hints, name, property.getType());
225229
});
226230
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -419,6 +419,10 @@ Method getSetter() {
419419
return this.setter;
420420
}
421421

422+
Field getField() {
423+
return this.field;
424+
}
425+
422426
}
423427

424428
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrarTests.java

+81-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import org.junit.jupiter.api.Test;
2929

3030
import org.springframework.aot.hint.ExecutableHint;
31+
import org.springframework.aot.hint.FieldHint;
3132
import org.springframework.aot.hint.RuntimeHints;
3233
import org.springframework.aot.hint.TypeHint;
3334
import org.springframework.aot.hint.TypeReference;
@@ -108,39 +109,39 @@ void registerHintsWhenJavaBean() {
108109
void registerHintsWhenJavaBeanWithSeveralConstructors() throws NoSuchMethodException {
109110
RuntimeHints runtimeHints = registerHints(WithSeveralConstructors.class);
110111
assertThat(runtimeHints.reflection().typeHints()).singleElement()
111-
.satisfies(javaBeanBinding(WithSeveralConstructors.class,
112-
WithSeveralConstructors.class.getDeclaredConstructor()));
112+
.satisfies(javaBeanBinding(WithSeveralConstructors.class)
113+
.constructor(WithSeveralConstructors.class.getDeclaredConstructor()));
113114
}
114115

115116
@Test
116117
void registerHintsWhenJavaBeanWithMapOfPojo() {
117118
RuntimeHints runtimeHints = registerHints(WithMap.class);
118119
assertThat(runtimeHints.reflection().typeHints()).hasSize(2)
119-
.anySatisfy(javaBeanBinding(WithMap.class, "getAddresses"))
120+
.anySatisfy(javaBeanBinding(WithMap.class).methods("getAddresses"))
120121
.anySatisfy(javaBeanBinding(Address.class));
121122
}
122123

123124
@Test
124125
void registerHintsWhenJavaBeanWithListOfPojo() {
125126
RuntimeHints runtimeHints = registerHints(WithList.class);
126127
assertThat(runtimeHints.reflection().typeHints()).hasSize(2)
127-
.anySatisfy(javaBeanBinding(WithList.class, "getAllAddresses"))
128+
.anySatisfy(javaBeanBinding(WithList.class).methods("getAllAddresses"))
128129
.anySatisfy(javaBeanBinding(Address.class));
129130
}
130131

131132
@Test
132133
void registerHintsWhenJavaBeanWitArrayOfPojo() {
133134
RuntimeHints runtimeHints = registerHints(WithArray.class);
134135
assertThat(runtimeHints.reflection().typeHints()).hasSize(2)
135-
.anySatisfy(javaBeanBinding(WithArray.class, "getAllAddresses"))
136+
.anySatisfy(javaBeanBinding(WithArray.class).methods("getAllAddresses"))
136137
.anySatisfy(javaBeanBinding(Address.class));
137138
}
138139

139140
@Test
140141
void registerHintsWhenJavaBeanWithListOfJavaType() {
141142
RuntimeHints runtimeHints = registerHints(WithSimpleList.class);
142143
assertThat(runtimeHints.reflection().typeHints()).singleElement()
143-
.satisfies(javaBeanBinding(WithSimpleList.class, "getNames"));
144+
.satisfies(javaBeanBinding(WithSimpleList.class).methods("getNames"));
144145
}
145146

146147
@Test
@@ -177,18 +178,20 @@ void registerHintsWhenHasNestedTypeNotUsedIsIgnored() {
177178
void registerHintsWhenWhenHasNestedExternalType() {
178179
RuntimeHints runtimeHints = registerHints(WithExternalNested.class);
179180
assertThat(runtimeHints.reflection().typeHints()).hasSize(3)
180-
.anySatisfy(
181-
javaBeanBinding(WithExternalNested.class, "getName", "setName", "getSampleType", "setSampleType"))
182-
.anySatisfy(javaBeanBinding(SampleType.class, "getNested"))
181+
.anySatisfy(javaBeanBinding(WithExternalNested.class)
182+
.methods("getName", "setName", "getSampleType", "setSampleType")
183+
.fields("name", "sampleType"))
184+
.anySatisfy(javaBeanBinding(SampleType.class).methods("getNested").fields("nested"))
183185
.anySatisfy(javaBeanBinding(SampleType.Nested.class));
184186
}
185187

186188
@Test
187189
void registerHintsWhenHasRecursiveType() {
188190
RuntimeHints runtimeHints = registerHints(WithRecursive.class);
189191
assertThat(runtimeHints.reflection().typeHints()).hasSize(2)
190-
.anySatisfy(javaBeanBinding(WithRecursive.class, "getRecursive", "setRecursive"))
191-
.anySatisfy(javaBeanBinding(Recursive.class, "getRecursive", "setRecursive"));
192+
.anySatisfy(
193+
javaBeanBinding(WithRecursive.class).methods("getRecursive", "setRecursive").fields("recursive"))
194+
.anySatisfy(javaBeanBinding(Recursive.class).methods("getRecursive", "setRecursive").fields("recursive"));
192195
}
193196

194197
@Test
@@ -203,24 +206,28 @@ void registerHintsWhenValueObjectWithRecursiveType() {
203206
void registerHintsWhenHasWellKnownTypes() {
204207
RuntimeHints runtimeHints = registerHints(WithWellKnownTypes.class);
205208
assertThat(runtimeHints.reflection().typeHints()).singleElement()
206-
.satisfies(javaBeanBinding(WithWellKnownTypes.class, "getApplicationContext", "setApplicationContext",
207-
"getEnvironment", "setEnvironment"));
209+
.satisfies(javaBeanBinding(WithWellKnownTypes.class)
210+
.methods("getApplicationContext", "setApplicationContext", "getEnvironment", "setEnvironment")
211+
.fields("applicationContext", "environment"));
208212
}
209213

210214
@Test
211215
void registerHintsWhenHasCrossReference() {
212216
RuntimeHints runtimeHints = registerHints(WithCrossReference.class);
213217
assertThat(runtimeHints.reflection().typeHints()).hasSize(3)
214-
.anySatisfy(javaBeanBinding(WithCrossReference.class, "getCrossReferenceA", "setCrossReferenceA"))
215-
.anySatisfy(javaBeanBinding(CrossReferenceA.class, "getCrossReferenceB", "setCrossReferenceB"))
216-
.anySatisfy(javaBeanBinding(CrossReferenceB.class, "getCrossReferenceA", "setCrossReferenceA"));
218+
.anySatisfy(javaBeanBinding(WithCrossReference.class).methods("getCrossReferenceA", "setCrossReferenceA")
219+
.fields("crossReferenceA"))
220+
.anySatisfy(javaBeanBinding(CrossReferenceA.class).methods("getCrossReferenceB", "setCrossReferenceB")
221+
.fields("crossReferenceB"))
222+
.anySatisfy(javaBeanBinding(CrossReferenceB.class).methods("getCrossReferenceA", "setCrossReferenceA")
223+
.fields("crossReferenceA"));
217224
}
218225

219226
@Test
220227
void registerHintsWhenHasUnresolvedGeneric() {
221228
RuntimeHints runtimeHints = registerHints(WithGeneric.class);
222229
assertThat(runtimeHints.reflection().typeHints()).hasSize(2)
223-
.anySatisfy(javaBeanBinding(WithGeneric.class, "getGeneric"))
230+
.anySatisfy(javaBeanBinding(WithGeneric.class).methods("getGeneric").fields("generic"))
224231
.anySatisfy(javaBeanBinding(GenericObject.class));
225232
}
226233

@@ -246,8 +253,9 @@ void registerHintsWhenHasMultipleNestedClasses() {
246253
void registerHintsWhenHasPackagePrivateGettersAndSetters() {
247254
RuntimeHints runtimeHints = registerHints(PackagePrivateGettersAndSetters.class);
248255
assertThat(runtimeHints.reflection().typeHints()).singleElement()
249-
.satisfies(javaBeanBinding(PackagePrivateGettersAndSetters.class, "getAlpha", "setAlpha", "getBravo",
250-
"setBravo"));
256+
.satisfies(javaBeanBinding(PackagePrivateGettersAndSetters.class)
257+
.methods("getAlpha", "setAlpha", "getBravo", "setBravo")
258+
.fields("alpha", "bravo"));
251259
}
252260

253261
@Test
@@ -260,9 +268,9 @@ void registerHintsWhenHasInheritedNestedProperties() {
260268
.containsExactlyInAnyOrder("getInheritedNested", "setInheritedNested");
261269
});
262270
assertThat(runtimeHints.reflection().getTypeHint(ExtendingProperties.class))
263-
.satisfies(javaBeanBinding(ExtendingProperties.class, "getBravo", "setBravo"));
271+
.satisfies(javaBeanBinding(ExtendingProperties.class).methods("getBravo", "setBravo").fields("bravo"));
264272
assertThat(runtimeHints.reflection().getTypeHint(InheritedNested.class))
265-
.satisfies(javaBeanBinding(InheritedNested.class, "getAlpha", "setAlpha"));
273+
.satisfies(javaBeanBinding(InheritedNested.class).methods("getAlpha", "setAlpha").fields("alpha"));
266274
}
267275

268276
@Test
@@ -275,11 +283,11 @@ void registerHintsWhenHasComplexNestedProperties() {
275283
.containsExactlyInAnyOrder("getCount", "setCount");
276284
});
277285
assertThat(runtimeHints.reflection().getTypeHint(ListenerRetry.class))
278-
.satisfies(javaBeanBinding(ListenerRetry.class, "isStateless", "setStateless"));
286+
.satisfies(javaBeanBinding(ListenerRetry.class).methods("isStateless", "setStateless").fields("stateless"));
279287
assertThat(runtimeHints.reflection().getTypeHint(Simple.class))
280-
.satisfies(javaBeanBinding(Simple.class, "getRetry"));
288+
.satisfies(javaBeanBinding(Simple.class).methods("getRetry").fields("retry"));
281289
assertThat(runtimeHints.reflection().getTypeHint(ComplexNestedProperties.class))
282-
.satisfies(javaBeanBinding(ComplexNestedProperties.class, "getSimple"));
290+
.satisfies(javaBeanBinding(ComplexNestedProperties.class).methods("getSimple").fields("simple"));
283291
}
284292

285293
@Test
@@ -292,17 +300,8 @@ void registerHintsDoesNotThrowWhenParameterInformationForConstructorBindingIsNot
292300
assertThatNoException().isThrownBy(() -> registerHints(PoolProperties.class));
293301
}
294302

295-
private Consumer<TypeHint> javaBeanBinding(Class<?> type, String... expectedMethods) {
296-
return javaBeanBinding(type, type.getDeclaredConstructors()[0], expectedMethods);
297-
}
298-
299-
private Consumer<TypeHint> javaBeanBinding(Class<?> type, Constructor<?> constructor, String... expectedMethods) {
300-
return (entry) -> {
301-
assertThat(entry.getType()).isEqualTo(TypeReference.of(type));
302-
assertThat(entry.constructors()).singleElement().satisfies(match(constructor));
303-
assertThat(entry.getMemberCategories()).isEmpty();
304-
assertThat(entry.methods()).extracting(ExecutableHint::getName).containsExactlyInAnyOrder(expectedMethods);
305-
};
303+
private JavaBeanBinding javaBeanBinding(Class<?> type) {
304+
return new JavaBeanBinding(type);
306305
}
307306

308307
private Consumer<TypeHint> valueObjectBinding(Class<?> type) {
@@ -318,7 +317,7 @@ private Consumer<TypeHint> valueObjectBinding(Class<?> type, Constructor<?> cons
318317
};
319318
}
320319

321-
private Consumer<ExecutableHint> match(Constructor<?> constructor) {
320+
private static Consumer<ExecutableHint> match(Constructor<?> constructor) {
322321
return (executableHint) -> {
323322
assertThat(executableHint.getName()).isEqualTo("<init>");
324323
assertThat(Arrays.stream(constructor.getParameterTypes()).map(TypeReference::of).toList())
@@ -804,4 +803,49 @@ public void setStateless(boolean stateless) {
804803

805804
}
806805

806+
private static final class JavaBeanBinding implements Consumer<TypeHint> {
807+
808+
private final Class<?> type;
809+
810+
private Constructor<?> constructor;
811+
812+
private List<String> expectedMethods = Collections.emptyList();
813+
814+
private List<String> expectedFields = Collections.emptyList();
815+
816+
private JavaBeanBinding(Class<?> type) {
817+
this.type = type;
818+
this.constructor = this.type.getDeclaredConstructors()[0];
819+
}
820+
821+
@Override
822+
public void accept(TypeHint entry) {
823+
assertThat(entry.getType()).isEqualTo(TypeReference.of(this.type));
824+
assertThat(entry.constructors()).singleElement().satisfies(match(this.constructor));
825+
assertThat(entry.getMemberCategories()).isEmpty();
826+
assertThat(entry.methods()).as("Methods requiring reflection")
827+
.extracting(ExecutableHint::getName)
828+
.containsExactlyInAnyOrderElementsOf(this.expectedMethods);
829+
assertThat(entry.fields()).as("Fields requiring reflection")
830+
.extracting(FieldHint::getName)
831+
.containsExactlyInAnyOrderElementsOf(this.expectedFields);
832+
}
833+
834+
private JavaBeanBinding constructor(Constructor<?> constructor) {
835+
this.constructor = constructor;
836+
return this;
837+
}
838+
839+
private JavaBeanBinding methods(String... methods) {
840+
this.expectedMethods = List.of(methods);
841+
return this;
842+
}
843+
844+
private JavaBeanBinding fields(String... fields) {
845+
this.expectedFields = List.of(fields);
846+
return this;
847+
}
848+
849+
}
850+
807851
}

0 commit comments

Comments
 (0)