Skip to content

Misleading exception message when unnecesarily declaring properties bean with Spring Beans DSL #22311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
piotr-karon opened this issue Jul 12, 2020 · 5 comments
Labels
status: invalid An issue that we don't feel is valid

Comments

@piotr-karon
Copy link

piotr-karon commented Jul 12, 2020

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"
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 12, 2020
@piotr-karon
Copy link
Author

piotr-karon commented Jul 12, 2020

Update.

Removing

bean<CallmeProperties>()

from beans definition resolves the problem.

So the problem is the misleading exception message.

@piotr-karon piotr-karon changed the title @ConstructorBinding not working with Spring Beans DSL Misleading exception message when unnecesarily declaring properties bean with Spring Beans DSL Jul 12, 2020
@snicoll
Copy link
Member

snicoll commented Jul 13, 2020

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.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Jul 13, 2020
@piotr-karon
Copy link
Author

piotr-karon commented Jul 14, 2020

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?

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 14, 2020
@snicoll
Copy link
Member

snicoll commented Jul 15, 2020

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.

@snicoll snicoll closed this as completed Jul 15, 2020
@snicoll snicoll added status: invalid An issue that we don't feel is valid and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels Jul 15, 2020
@wilkinsona
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

4 participants