Spring-boot: No metadata generated with @ConstructorBinding and Kotlin

Created on 17 Jul 2019  ·  6Comments  ·  Source: spring-projects/spring-boot

Generation of metadata for configuration properties doesn't work work with immutable var Kotlin properties.

Module: spring-boot-configuration-processor
Version: 2.2.0.M4
Build system: Maven
Language: Kotlin
Related to: #8762

_Property class:_

@ConfigurationProperties(prefix = "spring.pulsar")
data class PulsarProperties(      
   val serviceUrl: String = "pulsar://localhost:6650", 
   val listenerThreads: Int = 1,
   ...
)

_Maven plugin setup:_

            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>kapt</id>
                        <goals>
                            <goal>kapt</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>src/main/kotlin</sourceDir>
                            </sourceDirs>
                            <output></output>
                            <annotationProcessorPaths>
                                <annotationProcessorPath>
                                    <groupId>org.springframework.boot</groupId>
                                    <artifactId>spring-boot-configuration-processor</artifactId>
                                    <version>2.2.0.M4</version>
                                </annotationProcessorPath>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

_Generated spring-configuration.metadata.json:_

{
  "groups": [
    {
      "name": "spring.pulsar",
      "type": "sample.config.PulsarProperties",
      "sourceType": "sample.config.PulsarProperties"
    }
  ],
  "properties": [],
  "hints": []
}

... so it's missing properties completely.

In case that property is specified outside of the constructor (lateinit var property - like before #8762 was implemented) then metadata is generated properly.

In documentation there're mentioned some features are not working with kapt but I do believe this should work (and in case of immutable props also with defaults).

superseded bug

Most helpful comment

workaround - use @org.springframework.boot.context.properties.bind.DefaultValue annotation and do not use Kotlin default values.
Example:

@ConfigurationProperties(prefix = "spring.pulsar")
data class PulsarProperties(
        /**
         *  Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface
         *  to dynamically provide a service URL.
         *  It can be coma separated value in case of non-kubernetes deployment
         */
        @DefaultValue("pulsar://localhost:6650")
        val serviceUrl: String,

        /**
         * Thread pool used to manage the TCP connections with brokers.
         * If you're producing/consuming across many topics, you'll most likely be
         * interacting with multiple brokers and thus have multiple TCP connections opened.
         * Increasing the ioThreads count might remove the "single thread bottleneck",
         * though it would only be effective if such bottleneck is indeed present
         * (most of the time it will not be the case...).
         * You can check the CPU utilization in your consumer process,
         * across all threads, to see if there's any thread approaching 100% (of a single CPU core).
         */
        @DefaultValue("1")
        val ioThreads: Int,

        /**
         * Thread pool size when you are using the message listener in the consumer.
         * Typically this is the thread-pool used by application to process the messages
         * (unless it hops to a different thread). It might make sense to increase
         * the threads count here if the app processing is reaching the 1 CPU core limit.
         */
        @DefaultValue("1")
        val listenerThreads: Int = 1
)
{
  "groups": [
    {
      "name": "spring.pulsar",
      "type": "sample.config.PulsarProperties",
      "sourceType": "sample.config.PulsarProperties"
    }
  ],
  "properties": [
    {
      "name": "spring.pulsar.io-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool used to manage the TCP connections with brokers. If you're producing\/consuming across many topics, you'll most likely be interacting with multiple brokers and thus have multiple TCP connections opened. Increasing the ioThreads count might remove the \"single thread bottleneck\", though it would only be effective if such bottleneck is indeed present (most of the time it will not be the case...). You can check the CPU utilization in your consumer process, across all threads, to see if there's any thread approaching 100% (of a single CPU core).",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.listener-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool size when you are using the message listener in the consumer. Typically this is the thread-pool used by application to process the messages (unless it hops to a different thread). It might make sense to increase the threads count here if the app processing is reaching the 1 CPU core limit.",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.service-url",
      "type": "java.lang.String",
      "description": "Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface to dynamically provide a service URL. It can be coma separated value in case of non-kubernetes deployment",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": "pulsar:\/\/localhost:6650"
    }
  ],
  "hints": []
}

All 6 comments

Defaults definitely don't work as KAPT does not generate that information at all, see #15397. I am surprised there is the creation of a group but no properties. Can you please move that to a small sample I can run myself?

@snicoll sample: https://github.com/To-da/spring-boot-config-md-kotlin-immutable-props

observations: in case that I specify default value (by Kotlin way - not by Spring annotation) for the property with type String the metadata are not generated. For Int property it's ok (known issue is that default value is not retrieved from assigned value due to KAPT limitations).
In case that there's String property without assigned default value and after that second on it works.

It definitely needs more detailed tests.

workaround - use @org.springframework.boot.context.properties.bind.DefaultValue annotation and do not use Kotlin default values.
Example:

@ConfigurationProperties(prefix = "spring.pulsar")
data class PulsarProperties(
        /**
         *  Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface
         *  to dynamically provide a service URL.
         *  It can be coma separated value in case of non-kubernetes deployment
         */
        @DefaultValue("pulsar://localhost:6650")
        val serviceUrl: String,

        /**
         * Thread pool used to manage the TCP connections with brokers.
         * If you're producing/consuming across many topics, you'll most likely be
         * interacting with multiple brokers and thus have multiple TCP connections opened.
         * Increasing the ioThreads count might remove the "single thread bottleneck",
         * though it would only be effective if such bottleneck is indeed present
         * (most of the time it will not be the case...).
         * You can check the CPU utilization in your consumer process,
         * across all threads, to see if there's any thread approaching 100% (of a single CPU core).
         */
        @DefaultValue("1")
        val ioThreads: Int,

        /**
         * Thread pool size when you are using the message listener in the consumer.
         * Typically this is the thread-pool used by application to process the messages
         * (unless it hops to a different thread). It might make sense to increase
         * the threads count here if the app processing is reaching the 1 CPU core limit.
         */
        @DefaultValue("1")
        val listenerThreads: Int = 1
)
{
  "groups": [
    {
      "name": "spring.pulsar",
      "type": "sample.config.PulsarProperties",
      "sourceType": "sample.config.PulsarProperties"
    }
  ],
  "properties": [
    {
      "name": "spring.pulsar.io-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool used to manage the TCP connections with brokers. If you're producing\/consuming across many topics, you'll most likely be interacting with multiple brokers and thus have multiple TCP connections opened. Increasing the ioThreads count might remove the \"single thread bottleneck\", though it would only be effective if such bottleneck is indeed present (most of the time it will not be the case...). You can check the CPU utilization in your consumer process, across all threads, to see if there's any thread approaching 100% (of a single CPU core).",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.listener-threads",
      "type": "java.lang.Integer",
      "description": "Thread pool size when you are using the message listener in the consumer. Typically this is the thread-pool used by application to process the messages (unless it hops to a different thread). It might make sense to increase the threads count here if the app processing is reaching the 1 CPU core limit.",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": 1
    },
    {
      "name": "spring.pulsar.service-url",
      "type": "java.lang.String",
      "description": "Note that pulsar client also support reloadable [org.apache.pulsar.client.api.ServiceUrlProvider] interface to dynamically provide a service URL. It can be coma separated value in case of non-kubernetes deployment",
      "sourceType": "sample.config.PulsarProperties",
      "defaultValue": "pulsar:\/\/localhost:6650"
    }
  ],
  "hints": []
}

Same problem if you are using nested data class.

You have to add @ConstructorBinding on nested class to be able to have the documentation generated for it.

You have to add @ConstructorBinding on nested class to be able to have the documentation generated for it.

This annotation is not necessary for the nested data class, but the nested data class must be inside the outer class like this:

@ConstructorBinding
@ConfigurationProperties(“credentials”)
data class Credentials(
    val someClient: Credential
) {
    data class Credential(
        val clientId: String,
        val clientSecret: String
    )
}

The sample was built with a milestone of 2.2.0 but adding @ConstructorBinding generates the properties for both the NotOk and Ok POJO so I am going to close this now.

Was this page helpful?
0 / 5 - 0 ratings