The Spring Configuration Annotation Processor does not seem to properly pick up setters on some objects.
Specifically:
@ConfigurationProperties(prefix = "listener")
@Bean
SimpleMessageListenerContainer container(){
return new SimpleMessageListenerContainer();
}
only discovers:
| Property | Type |
|---|---|
| listener.acknowledge-mode | org.springframework.amqp.core.AcknowledgeMode |
| listener.auto-startup | java.lang.Boolean |
| listener.channel-transacted | java.lang.Boolean |
| listener.connection-factory | org.springframework.amqp.rabbit.connection.ConnectionFactory |
| listener.consumer-arguments | java.util.Map |
| listener.consumer-batch-enabled | java.lang.Boolean |
| listener.expose-listener-channel | java.lang.Boolean |
| listener.listener-id | java.lang.String |
| listener.phase | java.lang.Integer |
| listener.possible-authentication-failure-fatal | java.lang.Boolean |
| listener.queue-names | java.lang.String[] |
but is missing properties like concurrentConsumerCount, which only has a setter, but no getter.
This is quite unintuitive, as @ConfigurationProperties on @Bean will set those properties.
Reproducer: https://github.com/ciis0/spring-boot-34309
Comment From: ciis0
I have uploaded a reproducer here: https://github.com/ciis0/spring-boot-34309
Comment From: enimiste
Hi @ciis0,
Your reproducer project produces the exepected values :
All fields set: Reproducer{fieldWithSetterOnly='set', fieldWithSetterAndGetter='set'}
Here is the console logs :
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.2)
2023-02-24T16:35:34.479+01:00 INFO 1138 --- [ main] d.c.r.s.SpringBoot34309Application : Starting SpringBoot34309Application using Java 17.0.5 with PID 1138 (/Volumes/Data/Codes/Java/spring-boot-issue-34309/target/classes started by enimiste in /Volumes/Data/Codes/Java/spring-boot-issue-34309)
2023-02-24T16:35:34.484+01:00 INFO 1138 --- [ main] d.c.r.s.SpringBoot34309Application : No active profile set, falling back to 1 default profile: "default"
2023-02-24T16:35:35.152+01:00 INFO 1138 --- [ main] d.c.r.s.SpringBoot34309Application : Started SpringBoot34309Application in 1.397 seconds (process running for 2.624)
2023-02-24T16:35:35.154+01:00 INFO 1138 --- [ main] d.c.r.s.SpringBoot34309Application : All fields set: Reproducer{fieldWithSetterOnly='set', fieldWithSetterAndGetter='set'}
Option : maybe the missing properties have an incorrect syntaxe in your properties file.
A best practice is to use snake-case naming for properties.
Example :
repo.field-with-setter-only=value, field-with-setter-and-getter=value
.
Comment From: wilkinsona
@enimiste Thanks for trying to help. I think you may have misunderstood the problem. The sample is illustrating that a property with only a setter can be set by the configuration property binder at runtime but the property is not found by the configuration annotation processor that produces JSON metadata that describe the properties.
Comment From: enimiste
Hi @wilkinsona, thanks for your replay, i m new in open source contributions 😀.
Based on you comment, i did some researches based on spring projects source code reverse engineering and i found the issue cause. You found in the following my proposition.
Solution :
org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor::resolveJavaBeanProperties :
Stream<PropertyDescriptor<?>> resolveJavaBeanProperties(TypeElement type, ExecutableElement factoryMethod,
TypeElementMembers members) {
// First check if we have regular java bean properties there
Map<String, PropertyDescriptor<?>> candidates = new LinkedHashMap<>();
//As we can see, it fetches only the properties with a getter method
members.getPublicGetters().forEach((name, getters) -> {
VariableElement field = members.getFields().get(name);
ExecutableElement getter = findMatchingGetter(members, getters, field);
TypeMirror propertyType = getter.getReturnType();
register(candidates, new JavaBeanPropertyDescriptor(type, factoryMethod, getter, name, propertyType, field,
members.getPublicSetter(name, propertyType)));
});
//Solution : from my understanding, we should write some code here to fetch the other properties that has only setters
/*
members.getPublicSetters().forEach((name, setters) -> {
VariableElement field = members.getFields().get(name);
ExecutableElement setter = findMatchingGetter(members, setters, field);
TypeMirror propertyType = setter.getReturnType();
register(candidates, new JavaBeanPropertyDescriptor(type, factoryMethod, null, name, propertyType, field,
setter));
});
- The "register" function handles already the duplicate properties
- But it will not add the only defined setter properties, because of the how the "isCandidate > isProperty" is implemented (required getter!=null)
- Below the code of these two functions
*/
// Then check for Lombok ones
members.getFields().forEach((name, field) -> {
TypeMirror propertyType = field.asType();
ExecutableElement getter = members.getPublicGetter(name, propertyType);
ExecutableElement setter = members.getPublicSetter(name, propertyType);
register(candidates,
new LombokPropertyDescriptor(type, factoryMethod, field, name, propertyType, getter, setter));
});
return candidates.values().stream();
}
And in the class JavaBeanPropertyDescriptor :
@Override
protected boolean isProperty(MetadataGenerationEnvironment env) {
boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType());
return !env.isExcluded(getType()) && (getGetter() != null || getSetter() != null ) && (getSetter() != null || isCollection);
}
edited Thanks
Comment From: wilkinsona
Thanks for sharing your bindings, @enimiste. Unfortunately, we don't really have time to walk someone through an issue like this at this level of detail. If you're interested in contributing, and we would love to to do so, please keep a look out for issues labeled with ideal-for-contribution or first-timers-only. The former are issues that we think someone could tackle without a lot of knowledge of Spring Boot's code base. The latter are issues for someone who's never contributed before and where we've taken the time to provide a step-by-step explanation of the changes that are needed.
Comment From: enimiste
Thanks for these advices, i will follow them.
Comment From: philwebb
After some more consideration, I don't think we can treat this as a bug. Generating meta-data for all setters will create a lot of invalid properties. For example, classes might implement ApplicationContextAware, but it makes no sense to surface that as a property.
As @criztovyl discovered when working on #34590, there are explicit tests to ensure that setter only methods don't have meta-data generated.
I've opened #34616 to see if we can find a better way for users to indicate which properties should/shouldn't be included in the meta-data.
Generally speaking, we've found that adding @ConfigurationProperties to regular classes is fraught with problems. In Spring Boot itself, we create our own property classes then use the PropertyMapper utility to apply them. For example, the FlywayProperties class is mapped to org.flywaydb.core.api.configuration.FluentConfiguration in FlywayAutoConfiguration.configureProperties. Perhaps you can use this approach until #34616 is fixed.
Comment From: criztovyl
Generally speaking, we've found that adding
@ConfigurationPropertiesto regular classes is fraught with problems
So @ConfigurationProperties on @Bean would be considered an "eschew feature" (at least that is what kustomize calls such a feature that works but is not endorsed)?
Comment From: wilkinsona
It certainly has to be used with some care. Typically, you're using @ConfigurationProperties on a @Bean method because the method's return type is a class that you do not control and, therefore, cannot add @ConfigurationProperties to it. Such a type almost certainly will not have been designed with configuration property binding in mind. This can lead to limited documentation of the properties, unwanted or missing properties, and so on.
Comment From: nikhilniky
Since the processor relies on getter methods to identify properties, it may not discover these properties properly. or @value will work i think