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.
Comment From: wilkinsona
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.
Comment From: wilkinsona
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.
Comment From: wilkinsona
https://github.com/spring-projects/spring-boot/issues/33239 should be done before this.
Comment From: mhalbritter
FYI: I've done #33239.