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 lateinit property fails with kotlin.UninitializedPropertyAccessException #35603

Closed
piotrp opened this issue May 23, 2023 · 7 comments
Assignees
Labels
type: regression A regression from a previous release
Milestone

Comments

@piotrp
Copy link

piotrp commented May 23, 2023

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:
    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:
    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.
@piotrp piotrp changed the title Constructor binding of @ConfigurationProperties to a Kotlin data class with default values doesn't work Constructor binding of @ConfigurationProperties to a Kotlin data class requires @ConstructorBinding, doesn't work at all when class defines default values May 23, 2023
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 23, 2023
@snicoll
Copy link
Member

snicoll commented May 23, 2023

@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.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label May 23, 2023
@piotrp
Copy link
Author

piotrp commented May 23, 2023

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.

@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 May 23, 2023
@piotrp
Copy link
Author

piotrp commented May 23, 2023

Then it's a misuse of Spring Boot API, and previously it just happened to work?

@wilkinsona
Copy link
Member

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.

@wilkinsona
Copy link
Member

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

@wilkinsona wilkinsona added type: regression A regression from a previous release and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels May 25, 2023
@wilkinsona wilkinsona added this to the 3.0.x milestone May 25, 2023
@philwebb philwebb changed the title Constructor binding of @ConfigurationProperties to a Kotlin data class requires @ConstructorBinding, doesn't work at all when class defines default values Constructor binding of @ConfigurationProperties to a Kotlin data class requires @ConstructorBinding, doesn't work at all when class defines default values May 25, 2023
@wilkinsona
Copy link
Member

wilkinsona commented May 26, 2023

The problem with lateinit properties is occurring due to 5d21c36 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.

@wilkinsona wilkinsona changed the title Constructor binding of @ConfigurationProperties to a Kotlin data class requires @ConstructorBinding, doesn't work at all when class defines default values Constructor binding of @ConfigurationProperties to a lateinit property fails with kotlin.UninitializedPropertyAccessException May 26, 2023
@wilkinsona wilkinsona self-assigned this May 26, 2023
@wilkinsona
Copy link
Member

wilkinsona commented May 26, 2023

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.

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

4 participants