Version: Spring Boot 2.5.7
Add BeanProperty from sub class first ,then ignore exist BeanProperty in super class (only use single parameter in BiConsumer
),and finally bind to the properties of the super class, resulting in the loss of the subclass property extension.
private void addProperties(Class<?> type) {
while (type != null && !Object.class.equals(type)) {
Method[] declaredMethods = getSorted(type, Class::getDeclaredMethods, Method::getName);
Field[] declaredFields = getSorted(type, Class::getDeclaredFields, Field::getName);
addProperties(declaredMethods, declaredFields);
type = type.getSuperclass();
}
}
private void addMethodIfPossible(Method method, String prefix, int parameterCount,
BiConsumer<BeanProperty, Method> consumer) {
if (method != null && method.getParameterCount() == parameterCount && method.getName().startsWith(prefix)
&& method.getName().length() > prefix.length()) {
String propertyName = Introspector.decapitalize(method.getName().substring(prefix.length()));
consumer.accept(this.properties.computeIfAbsent(propertyName, this::getBeanProperty), method);
}
}
void addSetter(Method setter) {
if (this.setter == null || isBetterSetter(setter)) {
this.setter = setter;
}
}
Comment From: sbrannen
@snicoll, this issue is referring to code in Spring Boot's JavaBeanBinder.
Can you provide details on why you think this should be addressed in Framework?
Comment From: wilkinsona
Sorry, that's my fault. I got my binder classes mixed up. We'll transfer it back.
Comment From: wilkinsona
@jiangbingi Thanks for the report. Unfortunately, I don't think I understand the problem that you've described. Here is an example of a sub-class overriding a property:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.example.demo.Gh28917Application.ExtensionProperties;
@SpringBootApplication
@EnableConfigurationProperties(ExtensionProperties.class)
public class Gh28917Application {
public static void main(String[] args) {
SpringApplication.run(Gh28917Application.class, "--extension.example=test");
}
static class BaseProperties {
private String example;
public String getExample() {
return example;
}
public void setExample(String example) {
System.out.println("Base = " + example);
this.example = example;
}
}
@ConfigurationProperties("extension")
static class ExtensionProperties extends BaseProperties {
public void setExample(String example) {
System.out.println("Extension = " + example);
}
}
}
Running the above outputs Extension = test
.
Could you please provide a similar example that reproduces the problem you are seeing?
Comment From: telxs
package test;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@EnableConfigurationProperties(TestApplication.ExtensionProperties.class)
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, "--extension.field.group=my-group", "--extension.field.tag=my-tag");
}
static class BaseProperties {
private BaseField field;
public BaseField getField() {
return field;
}
public void setField(BaseField field) {
this.field = field;
}
static class BaseField {
private String group;
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
}
}
@ConfigurationProperties(prefix = "extension")
static class ExtensionProperties extends BaseProperties implements InitializingBean {
private BaseFieldExt field;
@Override
public BaseFieldExt getField() {
return field;
}
public void setField(BaseFieldExt field) {
this.field = field;
}
static class BaseFieldExt extends BaseField {
private String tag;
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (field != null) {
System.out.println("bind ExtensionProperties success:" + field.getGroup() + "," + field.getTag());
} else if (super.getField() != null) {
System.out.println("bind ExtensionProperties fail. super property is not null:" + super.getField().getGroup());
}
}
}
}
**Comment From: philwebb**
The binder is calling `ExtensionProperties.setField(...)` with a bound instance and `ExtensionProperties.field` is being set. You are calling `super.getField()` which is returning `BaseProperties.field` which has not been set. You probably want to update the setter in `ExtensionProperties` as follows:
```java
public void setField(BaseFieldExt field) {
this.field = field;
super.setField(field);
}
Comment From: telxs
@philwebb The reality is that ExtensionProperties.field has not been set.
@Override
public void afterPropertiesSet() throws Exception {
if (field != null) {
System.out.println("bind ExtensionProperties success:" + field.getGroup() + "," + field.getTag());
} else if (super.getField() != null) {
// Here ExtensionProperties.field is null, and BaseProperties.filed is not null
System.out.println("bind ExtensionProperties fail. super property is not null:" + super.getField().getGroup());
}
}
**Comment From: wilkinsona**
The behaviour appears to be inconsistent. On repeated runs, sometimes the success message is output and sometimes the fail message is output:
. _ _ __ _ _ /\ / ' __ _ () __ __ _ \ \ \ \ ( ( )_ | ' | '| | ' \/ _` | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ' |_| .|| ||| |_, | / / / / =========|_|==============|/=//// :: Spring Boot :: (v2.5.7)
2021-12-09 10:48:49.743 INFO 43098 --- [ main] com.example.demo.TestApplication : Starting TestApplication using Java 1.8.0_252 on wilkinsona-a01.vmware.com with PID 43098 (/Users/awilkinson/dev/workspaces/spring-projects/spring-boot/2.6.x/gh-28917/bin/main started by awilkinson in /Users/awilkinson/dev/workspaces/spring-projects/spring-boot/2.6.x/gh-28917) 2021-12-09 10:48:49.745 INFO 43098 --- [ main] com.example.demo.TestApplication : No active profile set, falling back to default profiles: default bind ExtensionProperties fail. super property is not null:my-group 2021-12-09 10:48:50.122 INFO 43098 --- [ main] com.example.demo.TestApplication : Started TestApplication in 0.65 seconds (JVM running for 0.946)
. _ _ __ _ _ /\ / ' __ _ () __ __ _ \ \ \ \ ( ( )_ | ' | '| | ' \/ _` | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ' |_| .|| ||| |_, | / / / / =========|_|==============|/=//// :: Spring Boot :: (v2.5.7)
2021-12-09 10:50:03.309 INFO 43112 --- [ main] com.example.demo.TestApplication : Starting TestApplication using Java 1.8.0_252 on wilkinsona-a01.vmware.com with PID 43112 (/Users/awilkinson/dev/workspaces/spring-projects/spring-boot/2.6.x/gh-28917/bin/main started by awilkinson in /Users/awilkinson/dev/workspaces/spring-projects/spring-boot/2.6.x/gh-28917) 2021-12-09 10:50:03.311 INFO 43112 --- [ main] com.example.demo.TestApplication : No active profile set, falling back to default profiles: default bind ExtensionProperties success:my-group,my-tag 2021-12-09 10:50:03.695 INFO 43112 --- [ main] com.example.demo.TestApplication : Started TestApplication in 0.646 seconds (JVM running for 0.94)
**Comment From: wilkinsona**
The sorting of the declared methods is the problem. Methods are sorted by name and there are two methods named `getField`. This results in their ordering being undefined and variable. The getter that goes first has a knock-on effect upon which setter is considered to be better.
**Comment From: telxs**
consider the better getter is determined by the type of field ? like this :
private boolean isBetterGetter(Method getter) { return this.getter != null && (this.getter.getName().startsWith("is") || this.field != null && this.field.getType().equals(getter.getReturnType())); } ``` And it's possible to add field when adding method.
Comment From: scottfrederick
In the example above, calling java.lang.Class.getDeclaringMethods
on the ExtensionProperties
class will return three methods:
BaseField getField()
BaseFieldExt getField()
void setField(BaseFieldExt field)
When the superclass BaseProperties
is inspected, getDeclaringMethods
will return two methods:
BaseField getField()
void setField(BaseField field)
When walking all the methods of the class and superclass, there are three methods named getField()
. The first one encountered will win and will influence which of the two setters is chosen. In the case of ExtensionProperties
, the BaseField getField()
method is a synthetic bridge method generated and added to the class by the compiler. This is why getDeclaringMethods
is returning a method with this signature even though it advertises that the methods it returns excludes inherited methods.
We should be able to exclude bridge methods from consideration when choosing getters and setters to enable use cases like this.