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

Document that configuration property binding to a Kotlin value class with a default is not supported #41693

Closed
apankowski opened this issue Aug 2, 2024 · 3 comments
Labels
Milestone

Comments

@apankowski
Copy link

apankowski commented Aug 2, 2024

Affects: Spring Boot 3.3.2, Spring 6.1.11

When using a value class property in @ConfigurationProperties with a default value and not providing its value in application configuration leads to an error during binding of configuration. (Tested with Kotlin 1.9.25.)

Reproducer:

package com.example

import org.springframework.boot.CommandLineRunner
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
import org.springframework.context.annotation.Bean

@JvmInline
value class EntityId(val value: Int)

@ConfigurationProperties(prefix = "test")
data class AppConfig(val entityId: EntityId = EntityId(1))

@SpringBootApplication
@EnableConfigurationProperties(AppConfig::class)
class TestApp {

  @Bean
  fun runner(config: AppConfig) = CommandLineRunner {
    println("Value: ${config.entityId}")
  }
}

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

Running this fails with:

2024-08-02 18:17:05.389 [restartedMain] DEBUG LoggingFailureAnalysisReporter {} : Application failed to start due to an exception

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'test' to com.example.AppConfig
	at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:391)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:354)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:339)
	at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:331)
	at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:316)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bindOrCreate(ConfigurationPropertiesBinder.java:101)
	at org.springframework.boot.context.properties.ConstructorBound.from(ConstructorBound.java:44)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.lambda$createBeanDefinition$1(ConfigurationPropertiesBeanRegistrar.java:97)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1277)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:951)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1237)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1180)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:904)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:782)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:542)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1355)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1185)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:971)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625)
	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
	at com.example.TestAppKt.main(TestApp.kt:30)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.AppConfig]: Illegal arguments for constructor
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:218)
	at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.instantiate(ValueObjectBinder.java:196)
	at org.springframework.boot.context.properties.bind.ValueObjectBinder.create(ValueObjectBinder.java:105)
	at org.springframework.boot.context.properties.bind.Binder.lambda$handleBindResult$0(Binder.java:366)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.AbstractList$RandomAccessSpliterator.tryAdvance(AbstractList.java:708)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
	at org.springframework.boot.context.properties.bind.Binder.fromDataObjectBinders(Binder.java:488)
	at org.springframework.boot.context.properties.bind.Binder.handleBindResult(Binder.java:365)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:351)
	... 43 common frames omitted
Caused by: java.lang.IllegalArgumentException: java.lang.NullPointerException: Cannot invoke "java.lang.Number.intValue()" because the return value of "sun.invoke.util.ValueConversions.primitiveConversion(sun.invoke.util.Wrapper, Object, boolean)" is null
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:70)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
	at org.springframework.beans.BeanUtils$KotlinDelegate.instantiateClass(BeanUtils.java:904)
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:189)
	... 58 common frames omitted
Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Number.intValue()" because the return value of "sun.invoke.util.ValueConversions.primitiveConversion(sun.invoke.util.Wrapper, Object, boolean)" is null
	at java.base/sun.invoke.util.ValueConversions.unboxInteger(ValueConversions.java:81)
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	... 62 common frames omitted

2024-08-02 18:17:05.392 [restartedMain] ERROR LoggingFailureAnalysisReporter {} : 

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

Description:

Failed to bind properties under 'test' to com.example.AppConfig:

    Reason: java.lang.NullPointerException: Cannot invoke "java.lang.Number.intValue()" because the return value of "sun.invoke.util.ValueConversions.primitiveConversion(sun.invoke.util.Wrapper, Object, boolean)" is null

Action:

Update your application's configuration

Ideally, this would work just like it works with defaults for non-value classes.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 2, 2024
@snicoll snicoll transferred this issue from spring-projects/spring-framework Aug 2, 2024
@mhalbritter mhalbritter changed the title Kotlin, configuration properties and value class with default value fails during binding Configuration properties fail to bind when using Kotlin's value classes Aug 5, 2024
@mhalbritter mhalbritter added type: bug A general bug theme: kotlin and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 5, 2024
@mhalbritter mhalbritter added this to the 3.2.x milestone Aug 5, 2024
@mhalbritter mhalbritter changed the title Configuration properties fail to bind when using Kotlin's value classes Configuration properties fail to bind when using Kotlin's value classes with a default Aug 5, 2024
@FredoNook
Copy link

FredoNook commented Aug 6, 2024

This issue also happens when value class in configuration properties and any properties have default values, not just value class property:

@JvmInline
value class EntityId(val value: Int)

@ConfigurationProperties(prefix = "test")
data class AppConfig(
    val entityId: EntityId,
    val anotherProperty: String = "default value",
)

In this case if entityId present in property sources but anotherProperty not, happens NPE on binding anotherProperty. If replace value class in this case by common class then everything works fine.

@apankowski
Copy link
Author

Reproducer for @FredoNook's case:

package com.example

import org.springframework.boot.CommandLineRunner
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
import org.springframework.context.annotation.Bean

@JvmInline
value class EntityId(val value: Int)

@ConfigurationProperties(prefix = "test")
data class AppConfig(
  val entityId: EntityId,
  val anotherProperty: String = "default value",
)

@SpringBootApplication
@EnableConfigurationProperties(AppConfig::class)
class TestApp {

  @Bean
  fun runner(config: AppConfig) = CommandLineRunner {
    println("Value: ${config.entityId}")
  }
}

fun main(args: Array<String>) {
  System.setProperty(
    "SPRING_APPLICATION_JSON",
    """
    {
      "test": {
        "entityId": 1
      }
    }
    """.trimIndent()
  )
  runApplication<TestApp>(*args)
}

@wilkinsona
Copy link
Member

As far as I can tell, there's nothing that we can do about this as it doesn't appear to be possible to create an instance of AppConfigwith the default value for entity ID. This appears to be the case both with reflection and when trying to use AppConfig directly.

Viewed from Java, AppConfig has three constructors:

public com.example.gh41693.AppConfig(int,kotlin.jvm.internal.DefaultConstructorMarker)
public com.example.gh41693.AppConfig(int,int,kotlin.jvm.internal.DefaultConstructorMarker)
private com.example.gh41693.AppConfig(int)

Only one of these, the first, returns a KFunction from ReflectJvmMapping.getKotlinFunction(Constructor). This KFunction has a single parameter which is an EntityId. It does not accept null as an argument for this parameter so there appears to be no way to create an AppConfig without providing an EntityId with a value.

As noted above, this works fine for a non-value class:

data class EntityId(val value: Int)

@ConfigurationProperties(prefix = "test")
data class AppConfig(val entityId: EntityId = EntityId(1))

In this case AppConfig now has a public zero-args constructor that can be used to create an instance, either through reflection or by calling new AppConfig().

In summary, this problem appears to be a limitation of how a Kotlin value class is mapped into Java. You may want to open a Kotlin issue to see if support for working with value classes from Java can be improved. Until then, I don't think there's anything that we can do here other than documenting the limitation. We can use this issue to add something here.

@wilkinsona wilkinsona changed the title Configuration properties fail to bind when using Kotlin's value classes with a default Document that configuration property binding to a value class with a default is not supported Sep 6, 2024
@wilkinsona wilkinsona added type: documentation A documentation update and removed type: bug A general bug labels Sep 6, 2024
@wilkinsona wilkinsona modified the milestones: 3.2.x, 3.2.10 Sep 6, 2024
@wilkinsona wilkinsona changed the title Document that configuration property binding to a value class with a default is not supported Document that configuration property binding to a Kotlin value class with a default is not supported Sep 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants