Summary
If a bean class contains multiple getter/setter methods with a same name but different types, the binding mechanism does not sort these methods properly which may lead to a non-deterministic/erroneous behaviour.
Affects v2.7.5 (current main).
Details
Let's have following bean classes:
static class Child extends Parent<ChildProperty> {
@Override
public ChildProperty getProperty() {
return null;
}
}
abstract static class Parent<T extends ParentProperty> {
abstract public T getProperty();
}
static class ChildProperty extends ParentProperty {
}
abstract static class ParentProperty {
}
When compiled, the Child class does have two getter methods. One from the Parent class with the ParentProperty return type, another from it's own implementation with the ChildPropertyReturnType.
$ javap Child.class
Compiled from "Child.java"
class Child extends Parent<ChildProperty> {
Child();
public ChildProperty getProperty();
public ParentProperty getProperty();
}
The binding mechanism in the JavaBeanBinder.Bean.addProperties() method, reads all declared methods via reflection and then tries to sort those methods by its name. Reason of sorting is non-determinism of the Class.getDeclaredMethods() reflection method. This issue was previously noted in https://github.com/spring-projects/spring-boot/issues/24068
https://github.com/spring-projects/spring-boot/blob/v2.7.5/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java#L130-L143
The problems is: Sorting is solely based on method name which does not (fully) remove the aforementioned non-determinism.
Additional details
- I've encountered this issue in Kotlin, where constructs like
abstract var property: Tare slightly more common. - Here is a test app FieldsOrderingTest.txt (please rename the
txtextension tojava) - Maybe unexpectedly Java compiler does not flag the
public ParentProperty getProperty()on theChildclass as abstract. The following code returns array of false values:java Object[] abstracts = Arrays.stream(Child.class.getDeclaredMethods()) .map(Method::getModifiers) .map(Modifier::isAbstract) .toArray(); // abstracts = {Object[2]@710} // 0 = {Boolean@712} false // 1 = {Boolean@712} falseThis means theJavaBeanBinder.Bean.isCandidate()method returns true for bothgetProperty()methods in theChildclass.
Comment From: philwebb
Thanks very much for raising the issue @vkuzel.
I think that the problem might be being caused by synthetic bridge methods that are added by the compiler. I've attempted to fix this, but it's unfortunately quite hard to write a test that consistently works. I'd appreciate it If you could try the latest SNAPSHOT release and let me know if it actually solved your issue for real.
Comment From: vkuzel
I'd appreciate it If you could try the latest SNAPSHOT release and let me know if it actually solved your issue for real.
Hey @philwebb I tested this in my environment and it works as expected. The non-bridge methods are filtered out, so the binding mechanism does not have issues with method resolution.
Thank you!
Comment From: wilkinsona
Thanks very much for taking the time to try a snapshot, @vkuzel. Much appreciated.