Expression based Autowire in Spring Boot (with Kotlin) - spring

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()
}
}
}

Related

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")}"

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.

How to use variables from application.yaml in SpringBoot kotlin

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
}

Spring Boot and Kotlin - How to validate a JSON object

I'm using Spring Boot in Kotlin.
I'm taking in some JSON string, parsing it with ObjectMapper however I want to validate it has everything as in per the model - namely id and s3FilePath are not blank or missing.
So this is the model I want to validate against:
#JsonIgnoreProperties(ignoreUnknown = true)
class MyModel {
var id : String = ""
var s3FilePath : String = ""
}
This is where I use that model:
class FirstMessage {
fun create(newMessage: String) : String {
val objectMapper = ObjectMapper()
val parsedMap : MyModel = objectMapper.readValue(newMessage, MyModel::class.java)
val result = MyModel()
result.id = parsedMap.id
result.s3FilePath = parsedMap.s3FilePath
return objectMapper.writeValueAsString(result)
}
}
And finally I have this test where I want to validate an exception:
#Test
fun incompleteDataReturnsException() {
var input = """{"missing": "parts"}"""
// FirstMessage().create(input) // Will make some assertion here here
}
Any help would be appreciated. I've just started using Spring and its pretty 'intense'.
Thanks.
p.s. If creating that model wrong/there's a better way, please let me know. I'm a little unsure if thats the correct way.
You should use data classes for the models. Also, use kotlin jacksonObjectMapper() instead of ObjectMapper(). Standard ObjectMapper will not work in Kotlin. Or inject ObjectMapper from Spring context. Add "com.fasterxml.jackson.module:jackson-module-kotlin" in your dependencies.
#JsonIgnoreProperties(ignoreUnknown = true)
data class MyModel (
val id : String,
val s3FilePath : String
)
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
class FirstMessage {
fun create(newMessage: String) : String {
val parsedMap : MyModel = jacksonObjectMapper().readValue(newMessage)
return jacksonObjectMapper().writeValueAsString(parsedMap)
}
}
class FirstMessageTest {
#Test
fun incompleteDataReturnsException() {
val input = """{"missing": "parts"}"""
assertThrows (MissingKotlinParameterException::class.java
{FirstMessage().create(input)} // Will make some assertion here here
}
#Test
fun `Should parse`() {
val input = """{"id":"id",
"missing": "parts",
"s3FilePath":"somePath"}"""
FirstMessage().create(input) // Will make some assertion here here
}
}
If I understood your question correct, you just want to check if your required properties are set. So I would suggest checking for that properties after you parsed the string with something like this:
class FirstMessage {
fun create(newMessage: String) : String {
val objectMapper = ObjectMapper()
// validation 1: your input is valid JSON
val parsedMap : MyModel = objectMapper.readValue(newMessage, MyModel::class.java)
// validation 2: check that your properties are set
if(parsedMap.id.isNullOrEmpty() ||
parsedMap.s3FilePath.isNullOrEmpty())
{
throw IllegalArgumentException("Invalid input")
}
val result = MyModel()
result.id = parsedMap.id
result.s3FilePath = parsedMap.s3FilePath
return objectMapper.writeValueAsString(result)
}
}
Depending of the scope, a nicer solution would be a new annotation like #NotEmpty that you set on the properties of your target class that are required and have a generic parser function which validates all the annotated fields on your parsed object and throws a better exception which says exactly which fields are missing.

Inject properties name into class anotation

Is it possible to inject property name into the procedureName?
im using spring boot.
Try to use the next the next construction:
procedureName = "${procedure}" but it doesnt work
Also to write the special PropertySourcesPlaceholderConfigurer i think it not a good idea .
#NamedStoredProcedureQueries({
#NamedStoredProcedureQuery(name = "test",
procedureName = "${procedure}",
parameters = {
})
})
public class R
try to get property from properties-test.yml
Spring properties used to inject values in bean properties like below,
public class ClassWithInjectedProperty {
#Value("${props.foo}")
private String foo;
}
you case is not valid for value injection.

Resources