Skip to content
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

Constructor binding of @ConfigurationProperties to a Kotlin Data Class with default values doesn't work any more #32416

Closed
davinkevin opened this issue Sep 18, 2022 · 8 comments
Assignees
Labels
type: regression A regression from a previous release
Milestone

Comments

@davinkevin
Copy link
Contributor

davinkevin commented Sep 18, 2022

Hello πŸ‘‹ ,

I searched for a similar issue, but didn't find one so I chose to open it… If I've missed it, sorry πŸ˜‡.

Following modification of @ConstructingBinding in 3.0.0-M1, I had to remove the annotation from our ConfigurationProperties classes:

// @ConstructingBinding
@ConfigurationProperties("podcastserver.externaltools")
data class ExternalTools(
        val ffmpeg: String = "/usr/local/bin/ffmpeg",
        val ffprobe: String = "/usr/local/bin/ffprobe",
        val rtmpdump: String = "/usr/local/bin/rtmpdump",
        val youtubedl: String = "/usr/local/bin/youtube-dl"
)

With this, the application fails to start with the following error

[app] Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
[app] 2022-09-18T15:01:49.280Z ERROR 1 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :
[app]
[app] ***************************
[app] APPLICATION FAILED TO START
[app] ***************************
[app]
[app] Description:
[app]
[app] Failed to bind properties under 'podcastserver.externaltools' to com.github.davinkevin.podcastserver.service.properties.ExternalTools:
[app]
[app]     Property: podcastserver.externaltools.ffmpeg
[app]     Value: "/opt/ffmpeg/ffmpeg"
[app]     Origin: System Environment Property "PODCASTSERVER_EXTERNALTOOLS_FFMPEG"
[app]     Reason: java.lang.IllegalStateException: No setter found for property: ffmpeg
[app]
[app] Action:
[app]
[app] Update your application's configuration

Because of the Kotlin nature, there is multiple constructors generated to support default values defined. I've tried to use the annotation on the constructor like this

@ConfigurationProperties("podcastserver.externaltools")
data class ExternalTools @ConstructingBinding constructor(
        val ffmpeg: String = "/usr/local/bin/ffmpeg",
        val ffprobe: String = "/usr/local/bin/ffprobe",
        val rtmpdump: String = "/usr/local/bin/rtmpdump",
        val youtubedl: String = "/usr/local/bin/youtube-dl"
)

but the result is not working too, for a different reason

[app] 2022-09-18T14:58:45.978Z ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed
[app]
[app] java.lang.IllegalStateException: com.github.davinkevin.podcastserver.service.properties.ExternalTools declares @ConstructorBinding on a no-args constructor
[app] 	at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider$Constructors.findAnnotatedConstructor(ConfigurationPropertiesBindConstructorProvider.java:135) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider$Constructors.getConstructors(ConfigurationPropertiesBindConstructorProvider.java:95) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider.getBindConstructor(ConfigurationPropertiesBindConstructorProvider.java:52) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBean$BindMethod.forType(ConfigurationPropertiesBean.java:325) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.createBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:92) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.registerBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:88) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:60) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:54) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
[app] 	at org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.registerBeanDefinitions(EnableConfigurationPropertiesRegistrar.java:49) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.context.annotation.ImportBeanDefinitionRegistrar.registerBeanDefinitions(ImportBeanDefinitionRegistrar.java:86) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:384) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[na:na]
[app] 	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:383) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:156) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:128) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:366) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:262) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:755) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:573) ~[spring-context-6.0.0-M5.jar:6.0.0-M5]
[app] 	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:430) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-3.0.0-M4.jar:3.0.0-M4]
[app] 	at com.github.davinkevin.podcastserver.PodcastServerApplicationKt.main(PodcastServerApplication.kt:17) ~[classes/:na]
[app]

The only way to make it working was to use the @ConstructorBinding constructor(…) with no default values… which is something we would like to avoid of course.

Thank you for your help on this subject.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Sep 18, 2022
@wilkinsona
Copy link
Member

Thanks for the report. It looks like these changes to ConfigurationPropertiesBindConstructorProvider went a little too far as we've lost the custom handling of Kotlin types that allows the primary constructor of a data class to be used for constructor-based binding.

@wilkinsona wilkinsona added type: regression A regression from a previous release and removed status: waiting-for-triage An issue we've not yet triaged labels Sep 21, 2022
@wilkinsona wilkinsona changed the title @ConfigurationProperties not working with Kotlin Data Class with default values in 3.0.0-M4 Constructor binding of @ConfigurationProperties to a Kotlin Data Class with default values doesn't work any more Sep 21, 2022
@wilkinsona wilkinsona added this to the 3.0.x milestone Sep 21, 2022
@wilkinsona wilkinsona self-assigned this Sep 21, 2022
@wilkinsona wilkinsona modified the milestones: 3.0.x, 3.0.0-M5 Sep 21, 2022
@kkocel
Copy link

kkocel commented Nov 15, 2022

@wilkinsona which version has this fix? I've tried 3.0.0-RC2 and the issue is still there.

My data class:

@ConfigurationProperties("foo.locale")
data class LocaleProperties @ConstructorBinding constructor(
    val default: String = "en-US",
    val supported: List<String> = listOf()
)

the exception I get:

Caused by: java.lang.IllegalStateException: com.example.LocaleProperties declares @ConstructorBinding on a no-args constructor
	at org.springframework.util.Assert.state(Assert.java:97)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructorBindingAnnotated(DefaultBindConstructorProvider.java:137)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructors(DefaultBindConstructorProvider.java:84)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:50)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBean$BindMethod.get(ConfigurationPropertiesBean.java:327)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.createBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:92)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.registerBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:88)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:60)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:54)
	at java.base/java.lang.Iterable.forEach(Iterable.java:75)
	at org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.registerBeanDefinitions(EnableConfigurationPropertiesRegistrar.java:49)
	at org.springframework.context.annotation.ImportBeanDefinitionRegistrar.registerBeanDefinitions(ImportBeanDefinitionRegistrar.java:86)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:373)
	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:372)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:148)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:409)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:283)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:745)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:565)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
	at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:134)
	at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:59)
	at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:47)
	at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1386)
	at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:526)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:134)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:105)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:183)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
	... 88 more

@wilkinsona
Copy link
Member

As shown by the milestone to which this issue is assigned, the fix was made in 3.0.0-M5.

I don't think you need @ConstructorBinding. In the problem description, @davinkevin tried that in an attempt to get things to work. If you look at the test in added in 6b8575b, @ConstructorBinding isn't used.

@mkosmul
Copy link

mkosmul commented Apr 19, 2023

@wilkinsona The issue seems to be back as of 3.0.5. The following data class:

@ConfigurationProperties("some.properties")
data class SomeProperties @ConstructorBinding constructor(val value: String? = null)

causes an exception during Spring context creation:

java.lang.IllegalStateException: Failed to load ApplicationContext for ...
...
Caused by: java.lang.IllegalStateException: pl.allegro.tech.selfservice.newservicesingle.domain.DomainConfiguration declares @ConstructorBinding on a no-args constructor
...

Adding an explicit no-args constructor without the annotation fixes the issue, but is unwieldy (brevity is an important selling point for using data classes for configuration properties, after all).

@ConfigurationProperties("some.properties")
data class SomeProperties @ConstructorBinding constructor(val value: String? = null) {
    constructor() : this(null)
}

Behavior is the same for org.springframework.boot.context.properties.bind.ConstructorBinding and for org.springframework.boot.context.properties.ConstructorBinding.

@wilkinsona
Copy link
Member

@mkosmul As per my comment immediately before yours, I don't think you need to use @ConstructorBinding.

@green-green-avk
Copy link

green-green-avk commented Aug 18, 2023

Hmm...

As of Spring Boot v3.1.2:

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties("app.someProps")
class SomeProps(
    val someProp: Boolean = true
)
...
Reason: java.lang.IllegalStateException: No setter found for property: some-prop
...

strikes again.

@philwebb
Copy link
Member

@green-green-avk Can you please open a new issue for this and provide a complete reproducer that we can run and debug?

@daliborfilus
Copy link

daliborfilus commented Feb 13, 2024

For anyone searching for this and discovering this issue via google:
@green-green-avk If I understand correctly, you need to remove @Component and add @EnableConfigurationProperties(SomeProps::class) to one of your other configuration components.
@Component alone throws No setter found for property messages for me too.

I.e. this works for me on spring boot 3.2.2:

@Configuration
@EnableConfigurationProperties(SomeProps::class)
class CorsConfiguration {
}

@ConfigurationProperties(prefix = "myproject.cors")
data class SomeProps(
    val enabled: Boolean = false,
)

And this doesn't:

@Configuration
//@EnableConfigurationProperties(SomeProps::class)
class CorsConfiguration {
}

@Component
@ConfigurationProperties(prefix = "myproject.cors")
data class SomeProps(
    val enabled: Boolean = false,
)

This version throws:

Failed to bind properties under 'myproject.cors' to ....config.SomeProps:

    Property: myproject.cors.enabled
    Value: "true"
    Origin: class path resource [application.yml] - 13:14
    Reason: java.lang.IllegalStateException: No setter found for property: enabled

Switch to the first version to make it work with val.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: regression A regression from a previous release
Projects
None yet
Development

No branches or pull requests

8 participants