I've got a library that has a configuration class (no spring configuration class) defined as a data class. I want a Bean of that configuration which can be configured via application.properties. The problem is that I don't know how to tell Spring to create ConfigurationProperties according to that external data class. I am not the author of the configuration class so I can't annotate the class itself. #ConfigurationProperties in conjunction with #Bean does not work as the properties are immutable. Is this even possible?
Maybe change scan packages to inlcude the packages that do you want.
#SpringBootApplication( scanBasePackages = )
take a look this:
Configuration using annotation #SpringBootApplication
If I understand correctly, do you need a way to turn a third-party object into a bean with properties from your application.properties file ?
Given an application.properties file:
third-party-config.params.simpleParam=foo
third-party-config.params.nested.nestedOne=bar1
third-party-config.params.nested.nestedTwo=bar2
Create a class to receive your params from properties file
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
#Configuration
#ConfigurationProperties(prefix = "third-party-config")
data class ThirdPartConfig(val params: Map<String, Any>)
Here is an example of the object that you want to use
class ThirdPartyObject(private val simpleParam: String, private val nested: Map<String, String>) {
fun printParams() =
"This is the simple param: $simpleParam and the others nested ${nested["nestedOne"]} and ${nested["nestedTwo"]}"
}
Create the configuration class with a method that turns your third-party object into an injectable bean.
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
#Configuration
class ThirdPartObjectConfig(private val thirdPartConfig: ThirdPartConfig) {
#Bean
fun thirdPartyObject(): ThirdPartyObject {
return ThirdPartObject(
simpleParam = thirdPartConfig.params["simpleParam"].toString(),
nested = getMapFromAny(
thirdPartConfig.params["nested"]
?: throw IllegalStateException("'nested' parameter must be declared in the app propertie file")
)
)
}
private fun getMapFromAny(unknownType: Any): Map<String, String> {
val asMap = unknownType as Map<*, *>
return mapOf(
"nestedOne" to asMap["nestedOne"].toString(),
"nestedTwo" to asMap["nestedTwo"].toString()
)
}
}
So now you can inject your third-party object as a bean with custom configurated params from your application.properties files
#SpringBootApplication
class StackoverflowAnswerApplication(private val thirdPartObject: ThirdPartObject): CommandLineRunner {
override fun run(vararg args: String?) {
println("Running --> ${thirdPartObject.printParams()}")
}
}
Related
i am new to Kotlin. I am trying to achieve same thing, i would do in java.
in application.yaml, i have this config:
conference:
plans:
- plan-id: P1_1
product-id: product1
employee-limit: 50
- plan-id: P1_2
product-id: product2
employee-limit: 100
then i want to create data class, so that , when i run the springboot application, this class is automatically loaded and validated
in Conferences.kt file:
package com.company.conferenceplanservice.config.properties
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotEmpty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.context.annotation.Configuration
import org.springframework.validation.annotation.Validated
#Validated
#ConstructorBinding
#Configuration
#ConfigurationProperties(prefix = "conference")
data class Plans(
#field:NotEmpty val plans: Set<Plan>
)
#Validated
#ConstructorBinding
data class Plan(
#field:NotBlank val planId: String,
#field:NotBlank val productId: String,
val eomployeeLimit: Int
)
but always it throws this exception, on both places where ConstructorBinding is written
Kotlin: This annotation is not applicable to target 'class'
springboot 3.0.0, java 18, kotlin 1.7.21
I hope this helps
I no longer use #ConstructingBinding nowadays
This is How I would have implemented
application.yaml
conference:
plan-id: P1_1
product-id: product1
Conferences.kt
import org.springframework.boot.context.properties.ConfigurationProperties
#ConfigurationProperties(prefix = "conference")
data class Conferences(
val planId: String,
val productId: String,
)
Usage.kt
object MySample {
fun haveFun(conference : Conference) : String {
return conference.planId
}
}
#Component
class BootLoader(val conference: Conference) : CommandLineRunner {
override fun run(vararg args: String?) {
println(MySample.haveFun(conference))
}
}
FYI
"#ConstructingBinding No Longer Needed at the Type Level"
"Improved #ConstructorBinding Detection When using constructor bound #ConfigurationProperties the #ConstructorBinding annotation is no longer required if the class has a single parameterized constructor. If you have more than one constructor, you’ll still need to use #ConstructorBinding to tell Spring Boot which one to use."
"For most users, this updated logic will allow for simpler #ConfigurationProperties classes. If, however, you have a #ConfigurationProperties and you want to inject beans into the constructor rather than binding it, you’ll now need to add an #Autowired annotation."
reference
https://github.com/spring-projects/spring-boot
https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConstructorBinding.html
#Validated
#ConstructorBinding
#Configuration
#ConfigurationProperties(prefix = "conference")
data class Plans(
#field:NotEmpty val plans: Set<Plan>
)
#Validated
#ConstructorBinding
data class Plan(
#field:NotBlank val planId: String,
#field:NotBlank val productId: String,
val eomployeeLimit: Int
)
and #ConfigurationPropertiesScan
annotation is needed in main class
I don't use Spring, but I'm guessing #ConstructorBinding is an annotation for a constructor. You are using it on the class declaration itself. It would be like putting it in front of the class name in Java:
#ConstructorBinding
public class Plans {
//...
}
If you want to annotate the data class constructor in Kotlin, you have to declare the constructor. You have omitted the constructor keyword, which is usually fine, but not if you need to annotate it or modify its visibility:
#Validated
#Configuration
#ConfigurationProperties(prefix = "conference")
data class Plans
#ConstructorBinding constructor(
#field:NotEmpty val plans: Set<Plan>
)
I am working with a custom AWS Simple System Management client just to avoid the original from using the default AWS authentication chain, so I placed my class in /META-INF/spring.factories and excluded the original from being autconfigured in bootstrap.yml . What I'm facing right now is to get the credentials from application.yml and pass them to my new conf, but when I try to autowire them all I get is null. I wonder if there is any way to achieve it
Here is the code:
package es.example;
import lombok.*;
import org.springframework.boot.context.properties.*;
#ConfigurationProperties(prefix = "aws.credentials")
#Data
public class CustomAWSSSMAuthProperties {
private String accessKey;
private String secretKey;
}
package es.example;
import com.amazonaws.services.simplesystemsmanagement.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.context.properties.*;
#EnableConfigurationProperties(CustomAWSSSMAuthProperties.class)
public class CustomAWSSSMClient extends AWSSimpleSystemsManagementClient {
#Autowired
private CustomAWSSSMAuthProperties customProperties;
public CustomAWSSSMClient() {
String accessKey = customProperties.getAccessKey();
String secretKey = customProperties.getSecretKey();
}
}
/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
es.example.CustomAWSSSMClient
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
es.example.CustomAWSSSMClient
bootstrap.yml
spring:
cloud:
config:
uri: ${SPRING_CONFIG_URI:http://localhost:8888}
autoconfigure.exclude: com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClient
Many thanks
#ConfigurationProperties does not create a bean like #Configuration or #Component does. In your case CustomAWSSSMAuthProperties type bean/object will not be instantiated in the Spring context.
Generally #ConfigurationProperties is used with #Configuration or #Bean to bind the some properties to the bean.
You can annotate CustomAWSSSMAuthProperties with #Configuration to fix the issue.
I'm trying to land a very simple project to see how complicated is to use Spring on Scala, but so far I can't really do the same stuff I simply did in Java, probably the way Scala instantiates the objects and such... So, some context regarding my environment.
#SpringBootApplication
object MyRunner {
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Clazz], args: _*)
}
}
Not much to say here, dummy class to launch the following class.
#Configuration
#ConfigurationProperties("sample")
#EnableConfigurationProperties
class Clazz #Autowired() (#BeanProperty var innerBean: InnerBean) extends Serializable with ApplicationRunner {
#BeanProperty
var property: String = _
override def run(args: ApplicationArguments): Unit = {
args.getSourceArgs
}
}
And here is where I'm having the issue related to my question. When I try to autowire InnerBean (#Autowired() (#BeanProperty var innerBean: InnerBean)), which is defined this way.
#Component
class InnerBean extends Serializable {
#BeanProperty
var beanValueOne: String = _
#BeanProperty
var beanValueTwo: String = _
}
It will just complain with Parameter 0 of constructor in project.impl.Clazz required a bean of type 'project.impl.InnerBean' that could not be found.. All those three classes are in the same .scala file.
Also, application.properties look like this.
sample.property=readingFromProperties
beanValueOne=readingFromProperties
beanValueTwo=readingFromProperties
I know that #SpringBootApplication already does a #ComponentScan in advance, so any beans within the package and below should be loaded for those to be available across?
The MyRunner object's SpringApplication.run call should reference a class with the #SpringBootApplication (which could be named 'MyRunner', but it's not necessary). Having the annotation on the object is probably where it is going wrong.
Ex:
#SpringBootApplication
class MyRunner
object MyRunner {
def main(args: Array[String]): Unit =
SpringApplication.run(classOf[MyRunner], args: _*)
}
#Configuration declares a source of #Beans
You've declared Clazz with #Configuration then tried to #Autowire in InnerBean which is marked with #Component. #Component to my knowledge is the same as #Bean but one is outside the scope of configuration so perhaps the point of generation is different in the Spring lifecycle.
How to fix...
Remove #Autowired() (#BeanProperty var innerBean: InnerBean) from your original code. Everything should compile and run if all else is well.
Put #Autowired() (#BeanProperty var innerBean: InnerBean) into a new class and create an instance of the class in the #Configuration file - by declaring a method that returns a NewClazz and annotating it with #Bean.
How to properly initialize ConfigurationProperties in Spring Boot with Kotlin?
Currently I do like in the example below:
#ConfigurationProperties("app")
class Config {
var foo: String? = null
}
But it looks pretty ugly and actually foo is not a variable, foo is constant value and should be initialized during startup and will not change in the future.
With new Spring Boot 2.2 you can do like so:
#ConstructorBinding
#ConfigurationProperties(prefix = "swagger")
data class SwaggerProp(
val title: String, val description: String, val version: String
)
And don't forget to include this in your dependencies in build.gradle.kts:
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
Here is how I have it working with my application.yml file.
myconfig:
my-host: ssl://example.com
my-port: 23894
my-user: user
my-pass: pass
Here is the kotlin file:
#Configuration
#ConfigurationProperties(prefix = "myconfig")
class MqttProperties {
lateinit var myHost: String
lateinit var myPort: String
lateinit var myUser: String
lateinit var myPass: String
}
This worked great for me.
Update: As of Spring Boot 2.2.0, you can use data classes as follows:
#ConstructorBinding
#ConfigurationProperties("example.kotlin")
data class KotlinExampleProperties(
val name: String,
val description: String,
val myService: MyService) {
data class MyService(
val apiToken: String,
val uri: URI
)
}
For further reference, see the official documentation.
Obsolete as of Spring Boot 2.2.0, Issue closed
As stated in the docs: A "Java Bean“ has to be provided in order to use ConfigurationProperties. This means your properties need to have getters and setters, thus val is not possible at the moment.
Getters and setters are usually mandatory, since binding is via standard Java Beans property descriptors, just like in Spring MVC. There are cases where a setter may be omitted [...]
This has been resolved for Spring Boot 2.2.0, which is supposed to be released soon:
https://github.com/spring-projects/spring-boot/issues/8762
On Spring Boot 2.4.3 with Kotlin 1.4.3 the next approach is no longer working (maybe because of a bug):
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
#SpringBootApplication
#EnableConfigurationProperties(TestProperties::class)
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
#ConfigurationProperties(prefix = "test")
#ConstructorBinding
data class TestProperties(
val value: String
)
The code above starts working after implying one of the next two approaches:
Add dependency
implementation("org.jetbrains.kotlin:kotlin-reflect")
Update Properties class
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
#ConfigurationProperties(prefix = "test")
data class TestProperties #ConstructorBinding constructor(
val value: String
)
The problem happens at the line org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java#68
#Value("\${some.property.key:}")
lateinit var foo:String
could be used this way
#ConstructorBinding
#ConfigurationProperties(prefix = "your.prefix")
data class AppProperties (
val invoiceBaseDir: String,
val invoiceOutputFolderPdf: String,
val staticFileFolder: String
)
Don't forget to add #ConfigurationPropertiesScan
#ConfigurationPropertiesScan
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
And finally the application.properties file:
your.prefix.invoiceBaseDir=D:/brot-files
your.prefix.invoiceOutputFolderPdf=invoices-pdf
your.prefix.staticFileFolder=static-resources
As of Spring Boot 3.0 you don't need to use #ConstructorBinding annotation anymore.
#ConfigurationProperties("app")
data class Config(
val foo: String = "default foo"
)
more info here
application.properties
metro.metro2.url= ######
Metro2Config.kt
#Component
#ConfigurationProperties(prefix = "metro")
data class Metro2PropertyConfiguration(
val metro2: Metro2 = Metro2()
)
data class Metro2(
var url: String ?= null
)
build.gradle
Plugins:
id 'org.jetbrains.kotlin.kapt' version '1.2.31'
// kapt dependencies required for IntelliJ auto complete of kotlin config properties class
kapt "org.springframework.boot:spring-boot-configuration-processor"
compile "org.springframework.boot:spring-boot-configuration-processor"
This is how I did it:
application.properties
my.prefix.myValue=1
MyProperties.kt
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
#Component
#ConfigurationProperties(prefix = "my.prefix")
class MyProperties
{
private var myValue = 0
fun getMyValue(): Int {
return myValue;
}
fun setMyValue(value: Int){
myValue = value
}
}
MyService.kt
#Component
class MyService(val myProperties: MyProperties) {
fun doIt() {
System.console().printf(myProperties.getMyValue().toString())
}
}
In addition to what's already been said, note that val and #ConstructorBinding has some limitations. You cannot alias one variable to another. Let's say you're running in Kubernetes and want to capture the hostname, which is given by the env var HOSTNAME. The easiest way to do this is to apply #Value("\${HOSTNAME}:)" to a property, but it only works for a mutable property and without constructor binding.
See #ConstructorBinding with immutable properties don't work with #Value in Spring Boot Kotlin #ConfigurationProperties.
I'm trying to create the following bean AmazonDynamoDBAsyncClientProvider. I've application.properties that defines endpoint and tablePrefix which I'm trying to inject using #ConfigurationProperties
Following is the code snippet for the same. When I run my spring-boot app it doesn't work.
I've tried doing the same ConfigurationProperties class using a regular java class which does set those properties but when it comes to AmazonDynamoDBAsyncClientProvider, the properties are empty. What am I missing here?
#Component
open class AmazonDynamoDBAsyncClientProvider #Autowired constructor(val dynamoDBConfiguration: DynamoDBConfig){
#Bean open fun getAmazonDBAsync() = AmazonDynamoDBAsyncClientBuilder.standard()
.withEndpointConfiguration(
AwsClientBuilder.EndpointConfiguration(dynamoDBConfiguration.endpoint, dynamoDBConfiguration.prefix))
.build()
}
here is the kotlin bean that I'm trying to autowire with configuration
#Component
#ConfigurationProperties(value = "dynamo")
open class DynamoDBConfig(var endpoint: String="", var prefix: String="")
finally heres the regular java bean that does get populated with ConfigurationProperties but when it gets Autowired I see those properties being empty/null
#Component
#ConfigurationProperties("dynamo")
public class DynamoDBConfiguration {
private String endpoint;
private String tablePrefix;
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getTablePrefix() {
return tablePrefix;
}
public void setTablePrefix(String tablePrefix) {
this.tablePrefix = tablePrefix;
}
}
Have you tried getting rid of the #Component annotation on your ConfigurationProperties class?
Here is what I have done with Kotlin and Spring, hope it helps.
I am trying to leverage the kotlin-spring and kotlin-allopen gradle plugin
dependencies {
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion"
}
apply plugin: 'kotlin-spring'
apply plugin: 'kotlin-noarg'
noArg {
annotation("your.noarg.annotation.package.NoArg")
}
They do make spring development with kotlin a lot easier.
#ConfigurationProperties("dynamo")
#NoArg
data class DynamoDBConfiguration(var endpoint: String, var prefix: String)
I tried your configuration class and it gets populated. I think your mistake is in the way you are trying to create the bean, the function needs to be in a class annotated with #Configuration, this should work:
#Configuration
class Beans {
#Bean
fun getAmazonDBAsync(config: DynamoDBConfiguration) =
AmazonDynamoDBAsyncClientBuilder.standard()
.withEndpointConfiguration(
AwsClientBuilder.EndpointConfiguration(config.endpoint, config.prefix)
)
.build()
}
Spring will inject the config for you, as long as you annotate the config with #Component, like you did above.
I had a similar problem and fixed it this way:
I defined the configuration properties class with lateinit vars:
#ConfigurationProperties(prefix = "app")
open class ApplicationConfigProperties {
lateinit var publicUrl: String
}
Then configured a bean in my spring boot application:
#SpringBootApplication
open class Application {
#Bean open fun appConfigProperties() = ApplicationConfigProperties()
}