I want duplicate configuration properties base on existing one, It works fine with 2.x but failed since 3.0.0, here is code snippet:

public class TestProperties {
        private String foo;
        private String bar;
       // getter setter
}

@ConfigurationProperties(prefix = "test")
public class MainTestProperties extends TestProperties {

}

@ConfigurationProperties(prefix = "another.test")
public class AnotherTestProperties extends TestProperties {
    public AnotherTestProperties(MainTestProperties properties) {
        BeanUtils.copyProperties(properties, this); //properties is null since v3.0.0
    }
}

Here is the full runnable unit test:

import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@TestPropertySource(properties = {"test.foo=foo", "test.bar=bar", "another.test.bar=anotherBar"})
class ConfigurationPropertiesTests {

    @Autowired
    private MainConfiguration mainConfiguration;

    @Autowired
    private AnotherConfiguration anotherConfiguration;


    @Test
    void test() {
        TestProperties mainProperties = mainConfiguration.getProperties();
        TestProperties anotherProperties = anotherConfiguration.getProperties();
        assertThat(mainProperties.getFoo()).isEqualTo("foo");
        assertThat(mainProperties.getBar()).isEqualTo("bar");
        assertThat(anotherProperties.getFoo()).isEqualTo("foo"); // copied from mainProperties
        assertThat(anotherProperties.getBar()).isEqualTo("anotherBar");
    }

    public static class TestProperties {

        private String foo;

        private String bar;

        public String getFoo() {
            return foo;
        }

        public void setFoo(String foo) {
            this.foo = foo;
        }

        public String getBar() {
            return bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }
    }

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(MainConfiguration.MainTestProperties.class)
    public static class MainConfiguration {

        private final TestProperties properties;

        public TestProperties getProperties() {
            return properties;
        }

        public MainConfiguration(MainTestProperties properties) {
            this.properties = properties;
        }

        @ConfigurationProperties(prefix = "test")
        public static class MainTestProperties extends TestProperties {

        }
    }


    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(AnotherConfiguration.AnotherTestProperties.class)
    public static class AnotherConfiguration {

        private final AnotherTestProperties properties;

        public TestProperties getProperties() {
            return properties;
        }

        public AnotherConfiguration(AnotherTestProperties properties) {
            this.properties = properties;
        }

        @ConfigurationProperties(prefix = "another.test")
        public static class AnotherTestProperties extends TestProperties {
            public AnotherTestProperties(MainConfiguration.MainTestProperties properties) {
                BeanUtils.copyProperties(properties, this);
            }
        }
    }

}

Here is a minimal project demo.zip

Comment From: quaff

This regression is fixed by adding explicit @Autowired on constructer.

Comment From: quaff

Mark the constructor as private works fine according to: https://github.com/spring-projects/spring-boot/blob/837ea04cd15b44e8f6e6485f78eac8f53ca8dfd7/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java#L180-L186

It's hard to distinguish implicit @Autowired constructor from implicit @ConstructorBinding constructor, and the use case is rare, I accept such breaking change totally. Spring Boot should update the javadoc of ConfigurationProperties and the migration guide.

Comment From: snicoll

I agree that the documentation is a bit light and could use something a bit more explicit, especially as flagging it private is also a way to opt-out.