I'm updating my application from Spring Boot 2.7 to 3.1, and I found what looks to be a resurrection/continuation of bug #32416: @ConfigurationProperties combined with Kotlin's data class fails in some scenarios.
Common test code:
@Configuration
@ConfigurationProperties(prefix = "redis")
class Configuration {
lateinit var redis1: RedisProperties
lateinit var redis2: RedisProperties
@Bean
fun redis1Connection(): RedisConnection = RedisConnection(redis1)
@Bean
fun redis2Connection(): RedisConnection = RedisConnection(redis2)
}
class RedisConnection(props: RedisProperties) {
val address: String = "${props.host}:${props.port}"
}
1. Data class without default values
@ConstructorBinding
data class RedisProperties(val host: String, val port: Int)
Spring Boot 2.7.12: works
Spring Boot 3.1.0:
* @ConstructorBinding is no longer valid here, removing it causes kotlin.UninitializedPropertyAccessException: lateinit property redis1 has not been initialized
* works with workaround, which shouldn't be necessary:
kotlin
data class RedisProperties @ConstructorBinding constructor(val host: String, val port: Int)
2. Data class with default values
@ConstructorBinding
data class RedisProperties(val host: String = "x", val port: Int = 0)
Spring Boot 2.7.12: works
Spring Boot 3.1.0:
* @ConstructorBinding is no longer valid here, removing it causes kotlin.UninitializedPropertyAccessException: lateinit property redis1 has not been initialized
* when I move @ConstructorBinding to constructor:
kotlin
data class RedisProperties @ConstructorBinding constructor(val host: String = "x", val port: Int = 0)
I get java.lang.IllegalStateException: com.example.demo.RedisProperties declares @ConstructorBinding on a no-args constructor.
"Workaround": remove default value from at least one field.
Comment From: snicoll
@piotrp have you reviewed the migration guide, in particular this section? Also, a ConfigurationProperties type shouldn't be a @Configuration class, that's mixing two completely different stereotypes.
Comment From: piotrp
Yes, and this indicates that @ConstructorBinding shouldn't be required, but looks like this isn't valid for Kotlin data classes, where constructor must be annotated (see my test cases above), at data classes which define defaults for all fields no longer work.
Comment From: piotrp
Then it's a misuse of Spring Boot API, and previously it just happened to work?
Comment From: wilkinsona
Then it's a misuse of Spring Boot API, and previously it just happened to work?
No, I don't think so. Stephane's comment about mixing @Configuration and @ConfigurationProperties was an aside. The problem you've described occurs without using @Configuration and @ConfigurationProperties on the same type.
Comment From: wilkinsona
This appears to be a regression in Spring Boot 3.0.5. Here's a minimal reproducer:
package com.example.gh35603
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
@SpringBootApplication
@EnableConfigurationProperties(RedisConfigurationProperties::class)
class Gh35603Application
fun main(args: Array<String>) {
runApplication<Gh35603Application>("--redis.redis1.host=test")
}
@ConfigurationProperties("redis")
class RedisConfigurationProperties {
lateinit var redis1: RedisProperties
lateinit var redis2: RedisProperties
data class RedisProperties(val host: String = "x", val port: Int = 0)
}
With 3.0.4, the application starts:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.4)
2023-05-25T14:45:51.280+01:00 INFO 55064 --- [ main] c.example.gh35603.Gh35603ApplicationKt : Starting Gh35603ApplicationKt using Java 17 with PID 55064 (/Users/awilkinson/dev/temp/gh-35603/build/classes/kotlin/main started by awilkinson in /Users/awilkinson/dev/temp/gh-35603)
2023-05-25T14:45:51.284+01:00 INFO 55064 --- [ main] c.example.gh35603.Gh35603ApplicationKt : No active profile set, falling back to 1 default profile: "default"
2023-05-25T14:45:51.754+01:00 INFO 55064 --- [ main] c.example.gh35603.Gh35603ApplicationKt : Started Gh35603ApplicationKt in 0.738 seconds (process running for 1.166)
It also works with 2.7.12 with @ConstructorBinding added to RedisProperties.
With 3.0.5 it fails:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.5)
2023-05-25T14:44:03.139+01:00 INFO 54853 --- [ main] c.example.gh35603.Gh35603ApplicationKt : Starting Gh35603ApplicationKt using Java 17 with PID 54853 (/Users/awilkinson/dev/temp/gh-35603/build/classes/kotlin/main started by awilkinson in /Users/awilkinson/dev/temp/gh-35603)
2023-05-25T14:44:03.143+01:00 INFO 54853 --- [ main] c.example.gh35603.Gh35603ApplicationKt : No active profile set, falling back to 1 default profile: "default"
2023-05-25T14:44:03.581+01:00 WARN 54853 --- [ main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'redis-com.example.gh35603.RedisConfigurationProperties': Could not bind properties to 'RedisConfigurationProperties' : prefix=redis, ignoreInvalidFields=false, ignoreUnknownFields=true
2023-05-25T14:44:03.586+01:00 INFO 54853 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-05-25T14:44:03.596+01:00 ERROR 54853 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'redis.redis1' to com.example.gh35603.RedisConfigurationProperties$RedisProperties:
Reason: kotlin.UninitializedPropertyAccessException: lateinit property redis1 has not been initialized
Action:
Update your application's configuration
Comment From: wilkinsona
The problem with lateinit properties is occurring due to https://github.com/spring-projects/spring-boot/commit/5d21c3616fe319323dfc67c05a41bef4087fb08f which, I think, has exposed another bug. Due to those changes, we now access the property to see if it already has a value. If it does, that means that we cannot use constructor binding as doing so would create a new instance. In this case, the property does not have a value but because it's a non-null lateinit property. Trying to access it throws an exception. We could, perhaps, just catch this exception somehow (it's Kotlin specific making that harder) and map that to a null value, but it might be better to call isInitialized on the property reference but I'm not sure if we can do that from Java.
Comment From: wilkinsona
it might be better to call isInitialized on the property reference but I'm not sure if we can do that from Java.
This doesn't appear to be possible through Kotlin's Java reflection support. While we can get a KProperty, isInitialized isn't available. We'll have to react to the kotlin.UninitializedPropertyAccessException instead.