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 @ConfigurationProperties to 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