Currently ConfigurationPropertiesBindConstructorProvider requires that @ConstructorBinding be used to indicate that a @ConfigurationProperties instance should use the ValueObjectBinder. I think we could skip the need for this if there is a single constructor with one or more arguments.

Somewhat related to #23172

Comment From: Davio

Would this mean that my @ConfigurationProperties classes which are currently annotated by Lombok's @Data (which creates getters and setters for my non-final fields) can be annotated by @Value instead (which creates final fields with an all args constructor)?

If so, I think that would be a reasonable improvement.

Comment From: snicoll

I think we could skip the need for this if there is a single constructor with one or more arguments.

I don't think we can/should. I remember us having a very lengthy conversation that concluded with the current arrangement. One problem with this approach is that you can't opt-out if you happen to have a single constructor and you don't want it to be used that way (can be surprising for folks having an existing class that uses javabean conventions and a constructor for whatever reason).

Comment From: Davio

Maybe it's an idea to consolidate @ConfigurationProperties and @ConstructorBinding by adding the binding mode as an optional property to @ConfigurationProperties or something like useConstructor = true which would work if there is a single constructor?

Would it work if the constructor only sets some of the properties, but others should still be set by setters?

Comment From: philwebb

@snicoll I remember some discussions, but I couldn't remember exactly why we landed on the current setup.

One problem with this approach is that you can't opt-out if you happen to have a single constructor and you don't want it to be used that way

Ah yes, I remember now. The issue is that you may have a @ConfigurationProperties class that is either a @Component or is loaded via a @Import. In those situations you want Spring to inject beans into the constructor and getter/setter binding to be applied afterwards.

Comment From: philwebb

Reopening to check why Spring Cloud need to make this change. The exception they have is:

08:34:17 Caused by: java.lang.IllegalStateException: Cannot bind @ConfigurationProperties for bean 'configServerHealthIndicator'. Ensure that @ConstructorBinding has not been applied to regular bean
08:34:17    at org.springframework.util.Assert.state(Assert.java:76)
08:34:17    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:86)
08:34:17    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:78)
08:34:17    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:425)
08:34:17    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1733)
08:34:17    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:416)
08:34:17    at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:105)
08:34:17    at org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration.afterSingletonsInstantiated(ConfigurationPropertiesRebinderAutoConfiguration.java:73)
08:34:17    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:945)
08:34:17    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:917)
08:34:17    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
08:34:17    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144)
08:34:17    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:720)
08:34:17    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:401)
08:34:17    at org.springframework.boot.SpringApplication.run(SpringApplication.java:302)
08:34:17    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:124)
08:34:17    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
08:34:17    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:123)
08:34:17    ... 39 common frames omitted

It seems like we should have detected that it was from a @Bean method.