Hello,

We were trying to migrate from 2.1.9 to 2.2.0 for our applications last week and encountered a startup failure due to a configuration validation failure. In short, if we are using @EnableConfigurationProperties combined with @ConditionalOnProperty for some beans, the new version is not respecting the conditional on the config part.

Here is a tiny example:

If we add this component to any Spring Boot application, it will fail to start with 2.2.0.

@Component
@ConditionalOnProperty(prefix = "foo", name = "enabled")
@EnableConfigurationProperties(FooBarConfig.class)
public class FooBar {

    public FooBar() {
        Logger.getGlobal().info("Starting FooBar...");
    }

    @ConfigurationProperties(prefix = "foo")
    @Validated
    static class FooBarConfig {

        @NotNull
        String bar;

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

    }

}

Expected behavior: The bean initialization of FooBar and FooBarConfig should be skipped because @ConditionalOnProperty doesn't match.

Actual behavior: FooBar is skipped but the FooBarConfig keeps initializing thus failed startup.

Comment From: snicoll

I understand that FooBar is in a package that is a target of component scan given the @Component on it. Spring Boot 2.2 now scans @ConfigurationProperties automatically and that arrangement above is really two separate components (i.e. excluding the parent doesn't mean that the inner class should not be scanned).

If you want to conditionally create such bean, you shouldn't put them in a package that is target for classpath scanning (auto-configurations shouldn't use package scanning in the first place). If you EnableConfigurationProperties(FooBar.class) from your auto-configuration, things should work fine in both versions.

This is based on partial code snippet so I might be missing something. Does that make sense?

Comment From: chengchen

@snicoll Thanks for the quick explanations. I see what you meant and I just noticed this on the upgrade instructions:

Classes annotated with @ConfigurationProperties can now be found via classpath scanning as an alternative to using @EnableConfigurationProperties or @Component. If you use @SpringBootApplication, scanning is enabled by default for the package that contains the @SpringBootApplication-annotated class.

We wanted to put the our service bean together with its configuration, and use @EnableConfigurationProperties to conditional switch on/off the bean initialization in a consistent way.

Comment From: snicoll

We wanted to put the our service bean together with its configuration, and use @EnableConfigurationProperties to conditional switch on/off the bean initialization in a consistent way.

And that's a perfectly reasonable thing to do. I can see now how scanning user-config makes it harder. Flagging for team attention to see how we could mitigate this.

Comment From: philwebb

One idea we discussed is adding a @SpringBootApplication(configurationPropertiesScanning=false) option to disable scanning. Another option would be a similar attribute on @ConfigurationProperties.

Comment From: derTobsch

I got a similar problem. We have a application that can talk with oidc provider or ldap e.g. and for each security provider we have a guarded configuration class (see here e.g. https://github.com/synyx/urlaubsverwaltung/blob/master/src/main/java/org/synyx/urlaubsverwaltung/security/oidc/OidcSecurityConfiguration.java) and this will only be executed if @ConditionalOnProperty(value = "uv.security.auth", havingValue = "oidc") is defined and therefore the @EnableConfigurationProperties(OidcSecurityProperties.class) will be collected and so on.

But now @EnableConfigurationProperties(OidcSecurityProperties.class) will be found even if v.security.auth does not have oidc. How is it now possible to only collect ConfigurationProperties based on a application property?

Comment From: wilkinsona

I'm starting to feel that configuration properties scanning may have been a mistake due to the dual purposes that @ConfigurationProperties now serves. I'd rather remove it than add an attribute to @SpringBootApplication or @ConfigurationProperties. As things stand, I don't think the benefit of no longer needing @EnableConfigurationProperties is worth the additional complexity of scanning and its side-effects.

Comment From: jeffbswope

My hunch when I was disabling this as we upgraded was that @ConfigurationProperties have ended up in an uncanny valley where the separate-but-not-quite-equal @Component/bean-like behavior appears to be causing confusion, here and elsewhere. Not sure if that gap can technically be closed.

Comment From: derTobsch

I'm starting to feel that configuration properties scanning may have been a mistake due to the dual purposes that @ConfigurationProperties now serves. I'd rather remove it than add an attribute to @SpringBootApplication or @ConfigurationProperties. As things stand, I don't think the benefit of no longer needing @EnableConfigurationProperties is worth the additional complexity of scanning and its side-effects.

I feel the same. I have at the moment no clue how I could achieve my requirement with Spring Boot 2.2. It feels like we "destroyed" the mechanism to have a "namespaced" configuration class with ConfigurationProperties support. That feels like a step back to @Value? :/

Comment From: evpaassen

Wouldn't a solution be to annotate the the configuration properties class with @ConditionalOnProperty and then annotate the service with @ConditionalOnBean, like this? (And I guess the classes should both be top-level then?)

@Component
@ConditionalOnBean(FooBarConfig.class)
@EnableConfigurationProperties(FooBarConfig.class)
public class FooBar {

    public FooBar() {
        Logger.getGlobal().info("Starting FooBar...");
    }
}

@ConditionalOnProperty(prefix = "foo", name = "enabled")
@ConfigurationProperties(prefix = "foo")
@Validated
public class FooBarConfig {

    @NotNull
    String bar;

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

Comment From: wilkinsona

Thanks for the suggestion, @evpaassen. We discourage use of @ConditionalOnBean in "normal" configuration and recommend that it's only used in auto-configuration where you have more guarantees about other beans having already been defined. It may be fine in this case as the bean is being defined via @EnableConfigurationProperties but it still risks being brittle or confusing people about when they can or cannot safely use @ConditionalOnBean.

Comment From: wilkinsona

We discussed this last week and decided that we're no longer going to enable configuration property scanning by default. Strictly speaking, this will be a breaking change from 2.2.0 but so was the loss of the ability to enable configuration properties conditionally. Making scanning opt-in will allow those that relied on 2.1.x's behaviour to continue to have it while also allowing those who want configuration property scanning to enable it by adding @ConfigurationPropertiesScan to their main application class alongside @SpringBootApplication.

Comment From: wilkinsona

I've broken some of the smoke tests.

Comment From: fkowal

So me 2.2.0 -> 2.2.1 migration is not going smoothly

I was using

@Configuration
@EnableConfigurationProperties(XXProperties::class)
class XXConfiguration {
}

@ConfigurationProperties(prefix = "xxprefix")
@ConstructorBinding
data class XXProperties(arg1: Arg1Class)

data class Arg1Class(...)

Now i am getting

Failed to bind properties under 'xxprefix' to org.example.XXProperties:

    Reason: Parameter specified as non-null is null: method org.example.XXProperties.<init>, parameter arg1

I am a bit confused about what changed and how to undo the changes from 2.2.1.

Better description in the release notes regarding breaking changes would be most welcome.

Comment From: snicoll

@fkowal thanks for letting us know but that looks like a regression unrelated to this issue. Please create a separate issue.

Comment From: steklopod

My example in kotlin & Spring Boot 2.2.1:

@Configuration
@ConfigurationPropertiesScan
class YamlParser(private val appInfo: ApplicationInfo, private val servletInfo: ServletInfo) {
    companion object{
        fun printDomain() = println(" \uD83C\uDFAF Target domain:  https://$domain")
    }
    @PostConstruct
    fun readApplicationProperties() {
        domain = appInfo.domain
        contentFolder = appInfo.contentFolder
        contextPath = servletInfo.contextPath
    }
}

@ConfigurationProperties(prefix = "application") @ConstructorBinding
data class ApplicationInfo(val domain: String, val contentFolder: String)

@ConfigurationProperties(prefix = "server.servlet") @ConstructorBinding
data class ServletInfo(val contextPath: String)

Comment From: emersonf

Are the 2.2 release notes correct?

The statement "[we] decided that we're no longer going to enable configuration property scanning by default" in this issue seems to contradict the statement "If you use @SpringBootApplication, scanning is enabled by default for the package that contains the @SpringBootApplication-annotated class." in the release notes. While pedantically this issue supersedes the 2.2 release notes if they're treated as 2.2.0 release notes, pragmatically the release notes will get far more eyeballs on them than this ticket.

Comment From: wilkinsona

Thanks, @emersonf. That was an oversight on our part. I've updated the release notes to hopefully clarify things.