I created a very, very simple project to test out configuration properties constructor binding. It sure doesn't seem to work at all. I've attached a zip file with the little project.

I also created this on stackoverflow but didn't get much help there: https://stackoverflow.com/questions/58259065/configurationproperties-constructor-binding-not-working-in-2-2-0-rc1

In the attached project, if I remove the "final" keyword on the "stuff" field, uncomment the setter method, and comment out the single argument constructor, the project runs fine (the application starts and the framework calls the setter). I've also tried commenting out the @Configuration annotation but that made no difference.

Anyway, when the application starts it fails with this error:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.acme.AppConfig required a bean of type 'java.lang.String' that could not be found.


Action:

Consider defining a bean of type 'java.lang.String' in your configuration.

Comment From: dldiehl77

spring-boot-acme.zip

Here is the sample project that fails to start.

Comment From: wilkinsona

Thanks for the sample. To opt in to constructor binding of configuration properties, you should use @ImmutableConfigurationProperties as described in the documentation.

You should not use @Configuration on a configuration properties class. @Configuration should be used to indicate that the class provides bean definitions via @Bean. @ConfigurationProperties or @ImmutableConfigurationProperties is sufficient.

Comment From: wilkinsona

I've opened https://github.com/spring-projects/spring-boot/issues/18545 to see if we can improve the failure analysis and the resulting error message.

Comment From: dldiehl77

I see my primary mistake. I was reading old documentation: https://docs.spring.io/spring-boot/docs/2.2.0.M2/reference/html/spring-boot-features.html#boot-features-external-config-constructor-binding

The new RC1 documentation has been updated and has the ImmutableConfigurationProperties annotation on the example. One small suggestion though: since you have import statements on that example, include the import statement for the ImmutableConfigurationProperties as well.

Comment From: philwebb

@dldiehl77 Thanks for raising that, I've opened https://github.com/spring-projects/spring-boot/issues/18547

Comment From: To-da

I do believe it should be updated also for Kotlin example: https://docs.spring.io/spring-boot/docs/2.2.0.RC1/reference/htmlsingle/#boot-features-kotlin-configuration-properties

Comment From: wilkinsona

Thank you, @To-da. I believe @sdeleuze has already taken care of that in https://github.com/spring-projects/spring-boot/pull/18573.

Comment From: perlun

Old issue, but I'm running into some weird issues with Spring Boot v2.4.0. When following this approach (non-immutable @ConfigurationProperties), all works fine: https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.java-bean-binding. I can look at the MyProperties instance in the debugger and I see that all values from application.yaml have been properly injected.

However, when I try the approach in https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.constructor-binding, i.e. this definition of the MyProperties class instead:

@ConfigurationProperties("my.service")
public class MyProperties {

    // fields...

    private final boolean enabled;

    private final InetAddress remoteAddress;

    private final Security security;

    public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }

    // getters...

    public boolean isEnabled() {
        return this.enabled;
    }

    public InetAddress getRemoteAddress() {
        return this.remoteAddress;
    }

    public Security getSecurity() {
        return this.security;
    }

    public static class Security {

        // fields...

        private final String username;

        private final String password;

        private final List<String> roles;

        public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }

        // getters...

        public String getUsername() {
            return this.username;
        }

        public String getPassword() {
            return this.password;
        }

        public List<String> getRoles() {
            return this.roles;
        }

    }

}

...I get a NoSuchBeanDefinitionException like this:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'boolean' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1777) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1333) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1287) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:885) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:228) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1206) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1367) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1287) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:885) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:228) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1206) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.1.jar:5.3.1]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:925) ~[spring-context-5.3.1.jar:5.3.1]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:588) ~[spring-context-5.3.1.jar:5.3.1]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) ~[spring-boot-2.4.0.jar:2.4.0]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) ~[spring-boot-2.4.0.jar:2.4.0]

The @ImmutableConfigurationProperties annotation seems to no longer exist in this version, likely because of https://github.com/spring-projects/spring-boot/issues/18563. I'll see if upgrading spring-boot to a later 2.x version might help...

Comment From: perlun

Upgrading turned out to be non-trivial (it caused other Prometheus-related issues on Spring startup), but I found the issue: @ConstructorBinding missing on the @ConfigurationProperties class is the missing part. Adding this makes it work as intended.

I am unsure if this is me or Spring TBH, but I opened this PR to ensure this isn't a matter of broken documentation: https://github.com/spring-projects/spring-boot/pull/34623

Comment From: wilkinsona

@perlun You are using Spring Boot 2.4.0 but referring to the current documentation which is for 3.0.4 at the time of writing. In 2.4, @ConstructorBinding is required (https://docs.spring.io/spring-boot/docs/2.4.x/reference/html/spring-boot-features.html#boot-features-external-config-constructor-binding). This was improved in 3.0 and @ConstructorBinding is no longer required in many cases (https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Release-Notes#improved-constructorbinding-detection).

Comment From: perlun

@wilkinsona Ahh... that explains it yes. :smile: I think what got me thinking in this direction was a link in https://stackoverflow.com/a/42371700/227779 which linked to the current version of the documentation => 3.0, and then I got confused when I couldn't use the approach described there in my project...

I'll edit the SO answer instead to help others who might still be stuck on 2.x. :+1: