In the process of build a stream processor I discovered the recommended use of @ConfigurationProperties for kotlin doesn't work as expected. My test-processor repo. https://github.com/corneil/test-processor

When building the Java version a file is produced named META-INF/spring-configuration-metadata.json containing:

{
  "groups": [
    {
      "name": "com.example.testprocessor",
      "type": "com.example.testprocessor.TestConfiguration",
      "sourceType": "com.example.testprocessor.TestConfiguration"
    }
  ],
  "properties": [
    {
      "name": "com.example.testprocessor.addition",
      "type": "java.lang.String",
      "description": "Will be added to name to make fullName",
      "sourceType": "com.example.testprocessor.TestConfiguration",
      "defaultValue": "N\/A"
    }
  ],
  "hints": []
}

The container image should have a label name org.springframework.boot.spring-configuration-metadata.json with all configuration properties and a label named org.springframework.cloud.dataflow.spring-configuration-metadata.json with the configuration properties from classes named in META-INF/dataflow-configuration-metadata.properties

{
  "groups": [
    { "name": "com.example.testprocessor", "type": "com.example.testprocessor.TestConfiguration", "sourceType": "com.example.testprocessor.TestConfiguration" },
    { "name": "com.example.testprocessor2", "type": "com.example.testprocessor.TestConfiguration2", "sourceType": "com.example.testprocessor.TestConfiguration2" }
  ],
  "properties": [
    {
      "name": "com.example.testprocessor.addition",
      "type": "java.lang.String",
      "description": "Will be added to name to make fullName",
      "sourceType": "com.example.testprocessor.TestConfiguration"
    }
  ]
}
  • When using the recommended data class the properties section doesn't contain the property com.example.testprocessor2.addition.
  • The defaultValue is missing from the property in the case where properies are detected.

Recommendation

@ConfigurationProperties(prefix = "com.example.testprocessor2")
data class TestConfiguration2(
    /**
     * Will be added to name to make fullName
     */
    val addition: String = "N/A"
)

Working version

@ConfigurationProperties(prefix = "com.example.testprocessor")
class TestConfiguration {
    /**
     * Will be added to name to make fullName
     */
    var addition: String = "N/A"
}

Reproduce

./build-only.sh
./build-images.sh
./inspect-images.sh

Comment From: philwebb

I think this one is probably a duplicate of #28046 or #16293. Do you agree @corneil?

Comment From: fprochazka

I just found this issue after trying to figure out why the official guide doesn't work, when applied on current versions (Gradle 8.10, Kotlin 2.0.20, JDK 22, Spring Boot 3.3.3)

Comment From: fprochazka

I've noticed the .java file that kapt generates in the gradle build directory and did some experiments:

when I set kapt.use.k2=true (in gradle.properties), and write the data class like this:

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.boot.context.properties.bind.DefaultValue

@ConfigurationProperties(prefix = QueryLoggingConfigurationProperties.PREFIX)
data class QueryLoggingConfigurationProperties @ConstructorBinding constructor(
    @DefaultValue("false")
    val enabled: Boolean = false,
) {

    companion object {
        const val PREFIX: String = "query.logging"
    }

}

then the spring-configuration-metadata.json is generated as expected.

Interestingly, when I mark the data class as @JvmRecord, the @ConstructorBinding was never propagated to the final .java file, only this specific incantation works.

Comment From: corneil

I think this one is probably a duplicate of #28046 or #16293. Do you agree @corneil?

My test used a data class with a single constructor with a single property, it excludes #16293 Maybe #28046.

The important difference from those is the APT for the configuration properties with the 'normal' class missed the defaultValue.

Comment From: corneil

I've noticed the .java file that kapt generates in the gradle build directory and did some experiments:

when I set kapt.use.k2=true (in gradle.properties), and write the data class like this:

```kotlin import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.bind.ConstructorBinding import org.springframework.boot.context.properties.bind.DefaultValue

@ConfigurationProperties(prefix = QueryLoggingConfigurationProperties.PREFIX) data class QueryLoggingConfigurationProperties @ConstructorBinding constructor( @DefaultValue("false") val enabled: Boolean = false, ) {

companion object {
    const val PREFIX: String = "query.logging"
}

} ```

then the spring-configuration-metadata.json is generated as expected.

Interestingly, when I mark the data class as @JvmRecord, the @ConstructorBinding was never propagated to the final .java file, only this specific incantation works.

Adding a @DefaultValue is good news. I would prefer not doing that if the annotation is not required for Java.

Comment From: fprochazka

After checking further, it seems that the @DefaultValue is not needed, the part that makes is work is using k2 and @ConstructorBinding constructor(

the k2 compiler for some reason generates multiple constructors in the .java file, and IMHO that's what confuses the processor. Adding the @ConstructorBinding probably forces it to focus on the correct constructor

Comment From: philwebb

My test used a data class with a single constructor with a single property, it excludes https://github.com/spring-projects/spring-boot/issues/16293

Although your Kolin class declares a single constructor, the generated bytecode has two. Here's the output from javap

$ javap -s com.example.testprocessor.TestConfiguration2

Compiled from "TestProcessor.kt"
public final class com.example.testprocessor.TestConfiguration2 {
  public com.example.testprocessor.TestConfiguration2(java.lang.String);
    descriptor: (Ljava/lang/String;)V

  public com.example.testprocessor.TestConfiguration2(java.lang.String, int, kotlin.jvm.internal.DefaultConstructorMarker);
    descriptor: (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

  public final java.lang.String getAddition();
    descriptor: ()Ljava/lang/String;

  public com.example.testprocessor.TestConfiguration2();
    descriptor: ()V
}

I think that makes this one a duplicate of #16293