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 nesting configuration properties using records or Kotlin data classes and how and when to use @NestedConfigurationProperty #33235

Closed
akefirad opened this issue Nov 17, 2022 · 4 comments
Assignees
Labels
type: documentation A documentation update
Milestone

Comments

@akefirad
Copy link

Having a simple Kotlin Spring Boot application (link), adding some data class as configuration properties class causes the native image to fail due to some reflection issues:

@ConfigurationProperties("bar")
data class BarProperties(val name: BarName)

data class BarName(val bar: String)

Stacktrace:

Caused by: kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Could not compute caller for function: public constructor BarName(bar: kotlin.String) defined in com.example.demo.BarName[DeserializedClassConstructorDescriptor@2cae19c5] (member = null)
        at kotlin.reflect.jvm.internal.KFunctionImpl$caller$2.invoke(KFunctionImpl.kt:88) ~[na:na]
        at kotlin.reflect.jvm.internal.KFunctionImpl$caller$2.invoke(KFunctionImpl.kt:61) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$LazyVal.invoke(ReflectProperties.java:63) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) ~[test-kotlin-native:1.7.20-release-201(1.7.20)]
        at kotlin.reflect.jvm.internal.KFunctionImpl.getCaller(KFunctionImpl.kt:61) ~[na:na]
        at kotlin.reflect.jvm.ReflectJvmMapping.getJavaConstructor(ReflectJvmMapping.kt:71) ~[na:na]
        at org.springframework.beans.BeanUtils$KotlinDelegate.findPrimaryConstructor(BeanUtils.java:852) ~[na:na]
        at org.springframework.beans.BeanUtils.findPrimaryConstructor(BeanUtils.java:282) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.deduceKotlinBindConstructor(DefaultBindConstructorProvider.java:172) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructors(DefaultBindConstructorProvider.java:89) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:50) ~[na:na]
        at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:42) ~[na:na]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.get(ValueObjectBinder.java:190) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:67) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder$ConstructorParameter.bind(ValueObjectBinder.java:314) ~[na:na]
        at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:76) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:332) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:324) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:309) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bindOrCreate(ConfigurationPropertiesBinder.java:101) ~[test-kotlin-native:3.0.0-RC2]
        at org.springframework.boot.context.properties.ConstructorBound.from(ConstructorBound.java:43) ~[na:na]
        at com.example.demo.BarProperties__BeanDefinitions.getBarPropertiesInstance(BarProperties__BeanDefinitions.java:24) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1225) ~[test-kotlin-native:6.0.0-RC4]
        ... 48 common frames omitted

Adding the following hint resolve the issue:

class NativeRuntimeHints : RuntimeHintsRegistrar {
    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
        hints.reflection().registerType(
            BarName::class.java,
            MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
        )
    }
}

I'm not sure if this is an issue in Spring Boot or Sprig Framework.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Nov 17, 2022
@wilkinsona wilkinsona changed the title Kotlin Data Class Properties Needs Runtime Hints Configuration property binding to a Kotlin data class fails without additional reflection hints Nov 17, 2022
@wilkinsona wilkinsona added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 17, 2022
@wilkinsona wilkinsona added this to the 3.0.x milestone Nov 17, 2022
@wilkinsona wilkinsona self-assigned this Nov 17, 2022
@wilkinsona
Copy link
Member

Looking at this more closely, the problem is that it's not clear that BarName is a nested configuration property. It should work if the code is structured like this:

@ConfigurationProperties("bar")
data class BarProperties(val name: BarName) {

    data class BarName(val bar: String)

}

It should also be possible to use @NestedConfigurationProperty to indicate that BarName should be treated as if it is nested. However, I don't think that's possible at the moment as @NestedConfigurationProperty can only be used on a field. We may have a similar problem with Java records.

@wilkinsona
Copy link
Member

Kotlin's smart enough to apply the @NestedConfigurationProperty to the field that's generated by the compiler. This works:

@ConfigurationProperties("bar")
data class BarProperties(@NestedConfigurationProperty val name: BarName)

data class BarName(val bar: String)

Java records are also OK. You can either nest the records:

record BarProperties(BarName name) {

    record BarName(String bar) {

    }

}

Or use the annotation:

record BarProperties(@NestedConfigurationProperty BarName name) {

}

record BarName(String bar)  {

}

We should add some examples to the documentation.

@wilkinsona wilkinsona added type: documentation A documentation update and removed type: bug A general bug labels Nov 17, 2022
@wilkinsona wilkinsona changed the title Configuration property binding to a Kotlin data class fails without additional reflection hints Document nesting configuration properties using records or Kotlin data classes and how and when to use @NestedConfigurationProperty Nov 17, 2022
@wilkinsona
Copy link
Member

#33239 should be done before this.

@mhalbritter
Copy link
Contributor

FYI: I've done #33239.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

4 participants