How to use variables from application.yaml in SpringBoot kotlin - spring

I need to use variables declared in my applications.yaml file, as an example all it is:
num_error:
value: "error"
result: 1
And I have a class trying to call it like the following:
#ConfigurationProperties(prefix = "num_error")
#Component
class NumError {
companion object {
lateinit var value: String
lateinit var result: Number
}
}
However, when I try and call this class using NumError.value I get an the following error
lateinit property value has not been initialized
kotlin.UninitializedPropertyAccessException: lateinit property value has not been initialized
What have I done wrong, why is this error happening?

You do not need to have companion object, and since Spring boot 2.2 you can have ConstructorBinding to make it work.
#ConstructorBinding
#ConfigurationProperties(prefix = "num_error")
data class NumError(
val value: String, val result: Number
)
Make sure you include following dependency
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
EDIT
For older versions, define the variables directly in the class instead of companion object.
#Configuration
#ConfigurationProperties(prefix = "num_error")
class NumError {
var value: String = "some default value",
var result: Number? = null
}

Related

Spring Boot Kotlin - Injecting a map from a YAML file

test.yml (location: resources/properties/)
edit:
field1: test
field2: test
field3: test
field4: test
PropertyConfig.kt
#Configuration
#PropertySource("classpath:properties/test.yml")
class PropertyConfig {
#Bean
#ConfigurationProperties(prefix = "edit")
fun testProperty() = mutableMapOf<String, String>()
}
#Service
class EditService(
private val testProperty: Map<String, String>
) {
fun print() {
println(testProperty) // empty
}
}
I want to receive the values below edit as a map.
I tried options for #ConfigurationProperties with prefix and value, but it doesn't work.
If I use properties file, it works well, but not yml file.
What am I missing? Thanks.
kotlinVersion = '1.6'; springBootVersion = '2.6.1'
You are missing the #ContructorBinding annotation (required as of Spring Boot 2.2.0). Please see this answer:
#ConstructorBinding
#ConfigurationProperties("")
data class PropertyConfig(
val edit: Map<String,String>
)
If you wanna use a non-standard yml file (not called application.yml or derivate), like in the example you provided, then you need to add also the #PropertySource annotation to your Configuration data class.
#ConstructorBinding
#ConfigurationProperties("")
#PropertySource(value = "classpath:test.yml")
data class PropertyConfig(
val edit: Map<String,String>
)
Something like this (without #Bean):
#ConfigurationProperties(prefix = "") // blank since "edit:" is root element
#ConstructorBinding
data class EditProperties(
val edit: Map<String, String> // property name must match to relevant root element in YAML
)
#Service
class EditService(private val properties: EditProperties) {
fun print() {
println(properties.edit)
}
}
Output:
{field1=test, field2=test, field3=test, field4=test}

How to interpolate property values provided by custom PropertySource in Spring Boot?

I have my custom FooPropertySources that extends EnumerablePropertySource. I add all of these in the #Configuration class to the ConfigurableEnvironment and they are correctly picked up be application and all the values are resolved.
However, if some values contain placeholders, they're not being interpolated. I thought I should use PropertySourcesPlaceholderConfigurer to solve that problem, but it seems like this configurer is meant to deal with placeholders in beans, rather than in property sources.
So far I tried this:
#Configuration
#ConditionalOnProperty("foo.config.import")
open class FooConfiguration {
#Autowired
private lateinit var env: ConfigurableEnvironment;
#Value("\${foo.config.import}")
private lateinit var locationSpecifier: String;
#PostConstruct
private fun initialize() {
val placeholderConfigurer = PropertySourcePlaceholderConfigurer();
val beanFactory = DefaultListableBeanFactory();
this.resolvePropertySources(this.parseLocationSpecifier())
.forEach(this.env.propertySources::addFirst);
placeholderConfigurer.setEnvironment(this.env);
placeholderConfigurer.postProcessBeanFactory(beanFactory);
}
internal fun resolvePropertySources(path: Path): Set<FooPropertySource> {
//...
return ...;
}
internal fun parseLocationSpecifier(): Path {
//...
return path;
}
}
Now, if an instance of FooPropertySource contains these properties:
firstname = John
lastname = Doe
fullname = ${firstname} ${lastname}
I'd like, in the end, when my application calls to env.getProperty("fullname") it will get the string "John Doe", rather than "${firstname} ${lastname}".
Any hopes to resolve that problem? I'm struggling with it for third day already… :-(
I guess you could create an extension function
fun ConfigurableEnvironment.fullname() = "${getProperty("firstname")} ${getProperty("lastname")}"

This annotation is not applicable to target 'local variable

I want to get value from application.yml, but I got "This annotation is not applicable to target 'local variable" for this part,how to solve this problem?
#Value("\${aws.secretsManager.secretName}")
val secretName: String? = ""
#Configuration
#EnableConfigurationProperties
#ConfigurationProperties
fun getSecret() {
#Value("\${aws.secretsManager.secretName}")
val secretName: String? = ""
val region = "us-west-2"
val logger: Logger = LoggerFactory.getLogger(GetSecretConfig::class.java)
// Create a Secrets Manager client
val client = AWSSecretsManagerClientBuilder.standard().withRegion(region).build()
val getSecretValueRequest = GetSecretValueRequest().withSecretId(secretName)
var getSecretValueResult: GetSecretValueResult? = try {
client.getSecretValue(getSecretValueRequest)
}
}
application.yml
aws:
secretsManager:
secretName: "test-mvp"
region: "us-west-2"
user: "root"
password: "root"
From the #Value javadoc:
Annotation used at the field or method/constructor parameter level that indicates a default value expression for the annotated element.
The #Value annotation is defined as follow:
#Target(value = {FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
#Retention(value = RUNTIME)
#Documented
public #interface Value
As you see by the #Target, the #Value annotation it's not intended to be used in a LOCAL_VARIABLE.
The solution is to define the secretName variable outside of the function - as a field of the class.
Workaround logic here:
(Using Java 8 though - shouldn't matter anyways)
Create a configuration class annotated with #Configuration like so:
#Configuration
public class ApplicationSecretsConfig {
public ApplicationSecretsConfig(){}
#Value("${aws.secretsManager.secretName}")
private String secretName;
public String getSecretName(){
return secretName;
}
}
Then in your class, autowire the SecretsConfig dependency and get the value of secretName using its getter.
// class initialization done here
...
#Autowired
ApplicationSecretsConfig applicationSecretsConfig
public String getSecret() {
String secret = applicationSecretsConfig.getSecretName();
// continue your logic
...
}
Hopefully that helps someone.
there is no need to do a custom implementation for fetch secrets. Spring provides it, using spring-cloud-starter-aws-secrets-manager-config dependency, just need to do an small config:
spring.config.import=aws-secretsmanager:my-secret
there is working sample on documentation:
https://github.com/awspring/spring-cloud-aws/tree/main/spring-cloud-aws-samples/spring-cloud-aws-parameter-store-sample
and here you could find a db working too:
https://github.com/nekperu15739/aws-secrets-manager

Autowired not working in Scala Spring Boot project

Taking into account the following example where I'm trying to use the Sample configuration bean within SampleStarter to start the service with the bean properly filled. The .scala file has SampleStarter.scala as name, with Sample being defined within that exact same file.
#SpringBootApplication
#Service
object SampleStarter {
#(Autowired #setter)
var sample: Sample = _ // always null, #Autowired not working?
def getInstance() = this
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
#Configuration
#ConfigurationProperties("sample")
#EnableConfigurationProperties
class Sample {
#BeanProperty
var myProperty: String = _
def run(): Unit = { // bean config seems OK, when debugging with #PostConstruct the 'myProperty' value is filled properly
print(myProperty)
}
}
Whenever I hit sample.run() after SpringApplication.run(classOf[Sample], args: _*), sample property is always null. I reckon this has something to do with object in Scala since all their members are static AFAIK. I took this SO question How to use Spring Autowired (or manually wired) in Scala object? as inspiration but still can't make my code to work.
Is there something wrong the way I'm instantiating the classes or is it something related to Scala?
EDIT
Following #Rob's answer, this is what I did trying to replicate his solution.
#SpringBootApplication
#Service
object SampleStarter {
#(Autowired #setter)
var sample: Sample = _
def getInstance() = this
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
#Configuration // declares this class a source of beans
#ConfigurationProperties("sample") // picks up from various config locations
#EnableConfigurationProperties // not certain this is necessary
class AppConfiguration {
#BeanProperty
var myProperty: String = _
#Bean
// Error -> Parameter 0 of constructor in myproject.impl.Sample required a bean of type 'java.lang.String' that could not be found.
def sample: Sample = new Sample(myProperty)
}
class Sample(#BeanProperty var myProperty: String) {
def run(): Unit = {
print(myProperty)
}
}
#Configuration declares a source file that is capable of providing #Beans. This is not what you want. You want/need to have Sample as a POJO class (POSO?) and then have another class "AppConfiguration" (say) annotated with #Configuration that creates an #Bean of type Sample
My scala is very rusty so this may not compile at all but the structure should be roughly correct.
#Configuration // declares this class a source of beans
#ConfigurationProperties("sample") // picks up from various config locations
#EnableConfigurationProperties // not certain this is necessary
class AppConfiguration {
#Bean
def getSample(#BeanProperty myProperty: String): Sample = new Sample(myProperty)
}
class Sample {
var myProperty: String
def Sample(property : String) = {
this.myProperty = myProperty
}
def run(): Unit = {
print(myProperty)
}
}
Now you have a Sample class as an #Bean and it can be #Autowired in where ever necessary.
#SpringBootApplication
#Service
object SampleStarter {
// note its typically better to inject variables into a constructor
// rather than directly into fields as the behaviour is more predictable
#Autowired
var sample: Sample
def getInstance() = this // not required ??
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
FWIW, you can start the SpringBoot application independently and have the logic for #Service entirely separate. An #Service is another type of #Bean and is made available to the rest of the SpringBootApplication.

Expression based Autowire in Spring Boot (with Kotlin)

Situation
I'm trying to come up with a methodology to conditionally load one bean (based on the existence of 2 property or environment variables) and if they are missing load up another bean.
Vars
So the two property (or env vars) are:
ProtocolHOST
ProtocolPORT
So for example java -jar xxxx -DProtocolHost=myMachine -DProtocolPort=3333 would use the bean I want, but if both are missing then you'd get another bean.
#Component("Protocol Enabled")
class YesBean : ProtocolService {}
#Component("Protocol Disabled")
class NoBean : ProtocolService {
Later in my controller I have a:
#Autowired
private lateinit var sdi : ProtocolService
So I've looked at a variety of options:
using both #ConditionalOnProperty and #ConditionalOnExpression and I cant seem to make any headway.
I'm pretty sure I need to go the Expression route so I wrote some test code that seems to be failing:
#PostConstruct
fun customInit() {
val sp = SpelExpressionParser()
val e1 = sp.parseExpression("'\${ProtocolHost}'")
println("${e1.valueType} ${e1.value}")
println(System.getProperty("ProtocolHost")
}
Which returns:
class java.lang.String ${ProtocolHost}
taco
So I'm not sure if my SPeL Parsing is working correctly because it looks like its just returning the string "${ProtocolHost}" instead of processing the correct value. I'm assuming this is why all the attempts I've made in the Expression Language are failing - and thus why i'm stuck.
Any assistance would be appreciated!
Thanks
Update
I did get things working by doing the following
in my main:
val protocolPort: String? = System.getProperty("ProtocolPort", System.getenv("ProtocolPort"))
val protocolHost: String? = System.getProperty("ProtocolHost", System.getenv("ProtocolHost"))
System.setProperty("use.protocol", (protocolHost != null && protocolPort != null).toString())
runApplication<SddfBridgeApplication>(*args)
And then on the bean definitions:
#ConditionalOnProperty(prefix = "use", name = arrayOf("protocol"), havingValue = "false", matchIfMissing = false)
#ConditionalOnProperty(prefix = "use", name = arrayOf("protocol"), havingValue = "false", matchIfMissing = false)
However this feels like a hack and I'm hoping it could be done directly in SpEL instead of pre-settings vars a head of time.
This sounds like a perfect use case for Java based bean configuration:
#Configuration
class DemoConfiguration {
#Bean
fun createProtocolService(): ProtocolService {
val protocolPort: String? = System.getProperty("ProtocolPort", System.getenv("ProtocolPort"))
val protocolHost: String? = System.getProperty("ProtocolHost", System.getenv("ProtocolHost"))
return if(!protocolHost.isNullOrEmpty() && !protocolPort.isNullOrEmpty()) {
YesBean()
} else {
NoBean()
}
}
}
open class ProtocolService
class YesBean : ProtocolService()
class NoBean : ProtocolService()
You might also want look into Externalized Configurations to replace System.getProperty() and System.getenv().
This would then look like this:
#Configuration
class DemoConfiguration {
#Bean
fun createProtocolService(#Value("\${protocol.port:0}") protocolPort: Int,
#Value("\${protocol.host:none}") protocolHost: String): ProtocolService {
return if (protocolHost != "none" && protocolPort != 0) {
YesBean()
} else {
NoBean()
}
}
}

Resources