Application with Bean DSL and @ConstructorBinding is unable to start due to org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pl.app.callme.CallMeProperties#0': @EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add @ConstructorBinding type pl.app.callme.CallMeProperties even though both (or one of them) are present.

App:

@SpringBootApplication
@EnableConfigurationProperties(CallMeProperties::class)
@ConfigurationPropertiesScan(basePackages = ["pl.app"])
class CallmeApp

fun main(){
    runApplication<CallmeApp> {
        addInitializers(appBeans())
    }
}

fun appBeans() = org.springframework.context.support.beans {
    bean<CallMeProperties>()

    bean {
        TestRouter(ref()).testRouter()
    }
}

@ConstructorBinding
@ConfigurationProperties(prefix = "callme")
data class CallMeProperties(
       var property1: String,
       var property2: String
)

Here's application.yml:

callme:
  property1: "Test1"
  property2: "Test2cfg"

And build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.3.1.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    kotlin("jvm") version "1.3.72"
    kotlin("plugin.spring") version "1.3.72"
    kotlin("kapt") version "1.3.72"
}

group = "pl.app.callme"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

extra["springCloudVersion"] = "Hoxton.SR6"

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.cloud:spring-cloud-starter-config")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
    runtimeOnly("io.micrometer:micrometer-registry-prometheus")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
    testImplementation("io.projectreactor:reactor-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

Full exception:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pl.app.callme.CallMeProperties#0': @EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add @ConstructorBinding type pl.app.callme.CallMeProperties
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.validate(ConfigurationPropertiesBeanDefinitionValidator.java:66) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.postProcessBeanFactory(ConfigurationPropertiesBeanDefinitionValidator.java:45) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:291) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:175) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:707) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:533) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:62) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
    at pl.app.callme.CallmeAppKt.main(CallmeApp.kt:36) ~[main/:na]
    at pl.app.callme.CallmeAppKt.main(CallmeApp.kt) ~[main/:na]

App written in traditional way works just fine:

@SpringBootApplication
@ConfigurationPropertiesScan
class CallmeApp

fun main(){
    runApplication<CallmeApp>()
}

@ConfigurationProperties(prefix = "callme")
@ConstructorBinding
class CallMeProperties(
        val property1: String,
        val property2: String
)

@RestController
@RequestMapping("/")
class CallmeController(
        private val properties: CallMeProperties
) {

    @GetMapping("/prop1")
    suspend fun getProp1() = properties.property1

    @GetMapping("/prop2")
    suspend fun getProp2() = properties.property2

}

application.yml:

callme:
  property1: "Test1"
  property2: "Test2cfg"

Comment From: piotr-karon

Update.

Removing

bean<CallmeProperties>()

from beans definition resolves the problem.

So the problem is the misleading exception message.

Comment From: snicoll

So the problem is the misleading exception message.

I am not sure what is misleading here. One one hand you've enabled component scanning for @ConfigurationProperties and on the other hand you've defined the bean yourself. That registers a bean for that particular type twice.

App written in traditional way works just fine:

It's not the same thing, is it? In this version you're not defining the bean yourself.

Comment From: piotr-karon

Well, I think I once got error indicating that I redeclared bean (but I may be wrong) so I would say this could be clearer.

What I wanted to use in first place was @ConstructorBinding annotation which requires @EnableConfigurationProperties or @ConfigurationPropertiesScan which requires @ConfigurationProperties which led to above situation. I see now that my intention was wrong.

So, maybe I should give up on @ConstructorBinding and use @Value annotations? This works as expected, but seems not so clean:

@SpringBootApplication
class CallmeApp

fun main(){
    runApplication<CallmeApp> {
        addInitializers(appBeans())
    }
}

fun appBeans() = org.springframework.context.support.beans {
    bean<CallMeProperties>()
    bean {
        TestRouter(ref()).testRouter()
    }
}

class CallMeProperties(
        @Value("\${app.property1}") val property1: String,
        @Value("\${app.property2}") val property2: String
)

Is this correct?

Comment From: snicoll

Is this correct?

Not really no. That sounds overcomplicated to what you had previously.

What I wanted to use in first place was @ConstructorBinding annotation which requires @EnableConfigurationProperties or @ConfigurationPropertiesScan which requires @ConfigurationProperties which led to above situation. I see now that my intention was wrong.

I have no idea how this led to the above situation. If you @EnableConfigurationProperties then you provide the class and we register a bean (so you don't have to). If you use @ConfigurationPropertiesScan then the class is scanned (so again, you don't have to register it). I don't understand why your @ConstructorBinding example is so different than your "traditional way". It does not have to be that way.

This works just fine and is very similar to what you call "traditional way".

@SpringBootApplication
@ConfigurationPropertiesScan
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

@ConstructorBinding
@ConfigurationProperties(prefix = "callme")
data class CallMeProperties(
        var property1: String,
        var property2: String
)

If you have more questions, please follow-up on StackOverflow or chat with the community on Gitter.

Comment From: wilkinsona

The changes made for #20798 mean that the error message that is produced has changed, hopefully for the better. With the latest snapshots, the failure looks like this:

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

Description:

CallMeProperties is annotated with @ConstructorBinding but it is defined as a regular bean which caused dependency injection to fail.

Action:

Update your configuration so that CallMeProperties is defined via @ConfigurationPropertiesScan or @EnableConfigurationProperties.