How to use same kotlin class to make 2 connections using configurationProperties - spring

Currently, My project consists of a config class having lateinit var and a connection class which uses this config to make connection to the DB. Here is dummy code of what it looks like for a single connection and it works fine-
Config.kt-
#Configuration
#ConfigurationProperties(prefix="db")
#Scope("singleton")
open class Config {
lateinit var schema: String
lateinit var account: String
}
Connection.kt-
#Configuration
open class Connection(private val config: Config) {
fun makeConn()
fun fetch()
}
application.properties-
db.schema=ABC
db.account=myAcc
main.kt-
open class dataFetch(
private val config: Config,
private val connection: Connection
) {
// Do something with config.schema
// Do something with connection.fetch()
}
Now I want to make 2 connection with the same config class values and connection class. Only the connection properties need to be changed. What is a good way of doing this? I have made 2 copies of both classes and made it work but it's ugly and repetitive.

Related

Testing Kotlin Extension Functions with Spring

I have a controller that gets a specific service based on a customer name:
#RestController
class BasicController(){
#Autowired
private lateinit var services: List<BasicService<*>>
private var service: BasicService<*>? = null
#GetMapping("/{customer}")
fun getAll(#PathVariable customer: String): ResponseEntity<String>{
service = services.getServiceByCustomer(customer)
/... code w/return value .../
}
}
I have a file Extensions.kt with the following:
fun <T: BasicService> List<T>.getServiceByCustomer(customer: String): T?{
return this.find{
it::class.simpleName?.contains(customer, ignoreCase = true) == true
}
}
Is it possible to return a mock of service when services.getServiceByCustomer is called similar to `when`(mock.function(anyString())).thenReturn(value)?
I've tried using mockK with the following:
mockkStatic("path.to.ExtensionsKt")
every {listOf(service).getServiceByCustomer)} returns service
But I don't think I'm using that properly... I'm currently using com.nhaarman.mockitokotlin2 but have tried io.mockk
You just need to use a customer that actually matches the simple name of the mocked service. You don't need or even should mock the extension function. Try the following:
class BasicControllerTest {
#MockK
private lateinit var basicService: BasicService
private lateinit var basicController: BasicController
#BeforeEach
fun setUp() {
clearAllMocks()
basicController = BasicController(listOf(basicService))
}
}
Additionally, consider using constructor injection instead of field injection:
#RestController
class BasicController(private val services: List<BasicService<*>>){
private var service: BasicService<*>? = null
#GetMapping("/{customer}")
fun getAll(#PathVariable customer: String): ResponseEntity<String>{
service = services.getServiceByCustomer(customer)
/... code w/return value .../
}
}
Finally, consider testing the Controllers with #WebMvcTest instead of regular unit tests. Check more info here https://www.baeldung.com/spring-boot-testing#unit-testing-with-webmvctest.

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

Spring auto configuration tries to set some bean values from application.properties while I just want to use that informations somewhere else?

So I use keycloak for my application and I have some values in application.properties like:
keycloak.auth-server-url = http://10.10.10.10:1010/auth
keycloak.resource = test-client
keycloak.credentials.secret = <very-big-secret>
keycloak.realm = test-realm
Spring configure the keycloak connection using these data, but I also use them in my code so I have a config like this:
#Data
#Configuration
#ConfigurationProperties(prefix = "keycloak")
public class KeycloakConfig {
private String authServerUrl;
private String realm;
private String resource;
private Credentials credentials;
}
I have an admin user in keycloak and I want it's credentials in the application.properties like this:
keycloak.admin.username=admin.admin
keycloak.admin.password=changeit
So I tried to change my config class to this:
#Data
#Configuration
#ConfigurationProperties(prefix = "keycloak")
public class KeycloakConfig {
private String authServerUrl;
private String realm;
private String resource;
private Credentials credentials;
private Admin admin;
}
#Data
public class Admin {
private String username;
private String password;
}
But when I try to run the application like this, I think the spring tries to set the values for keycloak (the .admin part) and it does not start:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target [Bindable#1cd5e41 type = org.keycloak.adapters.springboot.KeycloakSpringBootProperties, value = 'provided', annotations = array<Annotation>[#org.springframework.boot.context.properties.ConfigurationProperties(ignoreInvalidFields=false, ignoreUnknownFields=false, prefix=keycloak, value=keycloak)]] failed:
Property: keycloak.admin.password
Value: changeit
Origin: "keycloak.admin.password" from property source "applicationConfig: [classpath:/application.properties]"
Reason: The elements [keycloak.admin.password,keycloak.admin.username] were left unbound.
Property: keycloak.admin.username
Value: admin.admin
Origin: "keycloak.admin.username" from property source "applicationConfig: [classpath:/application.properties]"
Reason: The elements [keycloak.admin.password,keycloak.admin.username] were left unbound.
Action:
Update your application's configuration
Is it possible to have the .admin part under keycloak or I have to make a new class for example:
#Data
#Configuration
#ConfigurationProperties(prefix = "my-keycloak")
public class MyKeycloakConfig {
private Admin admin;
}
And:
my-keycloak.admin.username=admin.admin
my-keycloak.admin.password=changeit
I am not familiar with KeyCloak, but you can inject the bean that initialized by KeyCloak that reads the properties.
Keycloak reads values from application properties using KeycloakSpringBootProperties. Looks like there are no such values as username or password. Probably Keycloak doesn't require those values to work properly.
So you need to specify the properties seperately from keycloak.
No, you cannot customize keycloak.* "domain" in spring-boot (loaded) properties!
Proof: KeycloakSpringBootProperties, which says:
#ConfigurationProperties(prefix = "keycloak", ignoreUnknownFields = false)
So it is definitely the second approach!
By defining (in application.properties):
my-keycloak.admin.username=admin.admin
my-keycloak.admin.password=changeit
a) ... You can just go for:
#Value("${my-keycloak.admin.xxx}")
private String myKeacloakXXX;
b) Or as described by Typesafe Configuration Properties (and implemented by [1] for prefix="keycloak"):
You (just) have to introduce a "pojo" like (depicting your properties structure(type safe)):
#ConfigurationProperties("my-keycloak.admin")
public class MyKeykloakProperties {
private String username, password; // getter, setter/lombok
}
You can have also more structure with "my-keykloak" (prefix, and nesting classes/properties, see exmaple/doc)
To enable them:
#Configuration
// Or:
#EnableConfigurationProperties(MyKeykloakProperties.class)
// OR:
//#ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyKeycloakConfig { ...
see also Enabling.
Then you can "wire" them as you see fit (also in the above config class):
#Autowired
private MyKeykloakProperties properties;
As a decision help, please refer to: #Value vs type safe.
Cheers

SpringBoot RabbitMQ - how to reduce boilerplate for many topics (events)?

I wonder if there is a way to reduce amount of boilerplate code when initializing many RabbitMQ queues/bindings in SpringBoot?
Following event-driven approach, my app produces like 50 types of events (it will be split into several smaller apps later, but still).
Each event goes to exchange with type "topic".
Some events are getting consumed by other apps, some events additionally consumed by the same app which is sending them.
Lets consider that publishing-and-self-consuming case.
In SpringBoot for each event I need to declare:
routing key name in config (like "event.item.purchased")
queue name to consume that event inside the same app
("queue.event.item.purchased")
matching configuration properties class field or a variable itemPurchasedRoutingKey or constant in code which keeps property name (like ${event.item.purchased})
bean for Queue creation (with a name featuring event name) like
itemPurchasedQueue
bean for Binding creation (with a name featuring
event name) and routing key name. like itemPurchasedBinding which is
constructed with itemPurchasedQueue.bind(...itemPurchasedRoutingKey)
RabbitListener for event, with annotation containing queue name
(can't be defined in runtime)
So - 6 places where "item purchased" is mentioned in one or another form.
The amount of boilerplate code is just killing me :)
If there are 50 events, its very easy to make a mistake - when adding new event, you need to remember to add it to 6 places.
Ideally, for each event I'd like to:
specify routing key in config. Queue name can be built upon it by appending common prefix (specific to the app).
use some annotation or alternative RabbitListener which automatically declares queue (by routing key + prefix), binds to it, and listens to events.
Is there a way to optimize it?
I thought about custom annotations, but RabbitListener doesn't like dynamic queue names, and spring boot can't find beans for queues and bindings if I declare them inside some util method.
Maybe there is a way to declare all that stuff in code, but it's not a Spring way, I believe :)
So I ended up using manual bean declaration and using 1 bind() method for each bean
#Configuration
#EnableConfigurationProperties(RabbitProperties::class)
class RabbitConfiguration(
private val properties: RabbitProperties,
private val connectionFactory: ConnectionFactory
) {
#Bean
fun admin() = RabbitAdmin(connectionFactory)
#Bean
fun exchange() = TopicExchange(properties.template.exchange)
#Bean
fun rabbitMessageConverter() = Jackson2JsonMessageConverter(
jacksonObjectMapper()
.registerModule(JavaTimeModule())
.registerModule(Jdk8Module())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
)
#Value("\${okko.rabbit.queue-prefix}")
lateinit var queuePrefix: String
fun <T> bind(routingKey: String, listener: (T) -> Mono<Void>): SimpleMessageListenerContainer {
val queueName = "$queuePrefix.$routingKey"
val queue = Queue(queueName)
admin().declareQueue(queue)
admin().declareBinding(BindingBuilder.bind(queue).to(exchange()).with(routingKey)!!)
val container = SimpleMessageListenerContainer(connectionFactory)
container.addQueueNames(queueName)
container.setMessageListener(MessageListenerAdapter(MessageHandler(listener), rabbitMessageConverter()))
return container
}
internal class MessageHandler<T>(private val listener: (T) -> Mono<Void>) {
// NOTE: don't change name of this method, rabbit needs it
fun handleMessage(message: T) {
listener.invoke(message).subscribeOn(Schedulers.elastic()).subscribe()
}
}
}
#Service
#Configuration
class EventConsumerRabbit(
private val config: RabbitConfiguration,
private val routingKeys: RabbitEventRoutingKeyConfig
) {
#Bean
fun event1() = handle(routingKeys.event1)
#Bean
fun event2() = handle(routingKeys.event2)
...
private fun<T> handle(routingKey: String): Mono<Void> = config.bind<T>(routingKey) {
log.debug("consume rabbit event: $it")
... // handle event, return Mono<Void>
}
companion object {
private val log by logger()
}
}
#Configuration
#ConfigurationProperties("my.rabbit.routing-key.event")
class RabbitEventRoutingKeyConfig {
lateinit var event1: String
lateinit var event2: String
...
}

How can I inject config properties into a unit test, using SpringBoot2, JUnit5, and Kotlin

My scenario:
I'm building an app that uses Kotlin and SpringBoot 2.0.3. I'm trying to write all my unit tests in JUnit5. All 3 of these are new to me, so I'm struggling a bit.
I'm using a #ConfigurationProperties class (instead of #Value) to inject values from my application.yml into my Spring context.
#Configuration
#ConfigurationProperties(prefix = "amazon.aws.s3")
class AmazonS3Config {
val s3Enabled: Boolean = false
val region: String = ""
val accessKeyId: String = ""
val secretAccessKey: String = ""
val bucketName: String = ""
}
I then have a Kotlin class that is utilizing these properties, following Kotlin/Spring best practice to define the injected class as a constructor parameter.
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
override fun getInputStream(filePath: String): InputStream {
val region: String = amazonS3Config.region
val accessKeyId: String = amazonS3Config.accessKeyId
val secretAccessKey: String = amazonS3Config.secretAccessKey
val bucketName: String = amazonS3Config.bucketName
logger.debug { "The configured s3Enabled is: $s3Enabled" }
logger.debug { "The configured region is: $region" }
logger.debug { "The configured accessKeyId is: $accessKeyId" }
logger.debug { "The configured secretAccessKey is: $secretAccessKey" }
logger.debug { "The configured bucketName is: $bucketName" }
val file: File? = File(filePath)
//This method is not yet implemented, just read a file from local disk for now
return file?.inputStream() ?: throw FileNotFoundException("File at $filePath is null")
}
}
I have not completed this implementation, as I'm trying to get the unit test working first. So for the moment, this method doesn't actually reach out to S3, just streams a local file.
My unit test is where I'm getting stuck. I don't know how to inject the properties from my application.yml into the test context. Since the ConfigProperty class is passed as a construction parameter, I have to pass it when I establish my service in my unit test. I've tried various solutions that don't work. I found this piece of info, which was helpful:
If Spring Boot is being used, then #ConfigurationProperties instead of #Value annotations can be used, but currently this only works with lateinit or nullable var properties (the former is recommended) since immutable classes initialized by constructors are not yet supported.
So this means I cannot use class VqsS3FileReaderTest(amazonS3Config: AmazonS3Config): TestBase() { ... } and then pass the config to my service.
This is what I have currently:
#ActiveProfiles("test")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
#Autowired
private lateinit var amazonS3Config: AmazonS3Config
#Autowired
private lateinit var fileReader: VqsS3FileReader
val filePath: String = "/fileio/sampleLocalFile.txt"
#Test
fun `can get input stream from a valid file path` () {
fileReader = VqsS3FileReader(amazonS3Config)
val sampleLocalFile: File? = getFile(filePath) //getFile is defined in the TestBase class, it just gets a file in my "resources" dir
if (sampleLocalFile != null) {
val inStream: InputStream = fileReader.getInputStream(sampleLocalFile.absolutePath)
val content: String = inStream.readBytes().toString(Charset.defaultCharset())
assert.that(content, startsWith("Lorem Ipsum"))
} else {
fail { "The file at $filePath was not found." }
}
}
}
With this, my test runs, and my context seems to setup properly, but the properties from my application.yml are not being injected. For my debug output, I see the following:
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured s3Enabled is: false
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured region is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured accessKeyId is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured secretAccessKey is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured bucketName is:
All empty strings, which is the default values. Not the values I have in my application.yml:
amazon.aws.s3:
s3Enabled: true
region: us-west-2
accessKeyId: unknown-at-this-time
secretAccessKey: unknown-at-this-time
bucketName: test-bucket
I see mistake in the following line:
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
Please put configuration classes here (instead of just beans).
Short - hot to fix test
Create class (if missing) like VqsS3Configration in the main module (e.g. in the module, where you have production code)
Create class like VqsS3TestConfigration in the same package with your tests. Content on this file:
#org.springframework.context.annotation.Configuration // mark, that this is configuration class
#org.springframework.context.annotation.Import(VqsS3Configration::class) // it references production configuration from test configuration
#org.springframework.context.annotation.ComponentScan // ask Spring to autoload all files from the package with VqsS3TestConfigration and all child packages
class VqsS3TestConfigration {
/*put test-related beans here in future*/
}
Then go to test and change declaration:
#ContextConfiguration(classes = [VqsS3TestConfigration ::class]) // we ask Spring to load configuration here
I created sample application here: https://github.com/imanushin/spring-boot2-junit5-and-kotlin-integration
Please execude line .\gradlew.bat test or gradlew.bat bootRun in the src folder. Test will check, that we able to read properties. bootRun will print auto-loaded properties
Boring theory
First of all - Spring has Configuration classes - they are needed to load and initialize other classes. Instead of Service or Comonent classes, main purpose of Configuration classes - just create services, components, etc.
If we will simplify algorithm of the Spring application load, then it will be like this:
Find Configuration classes
Read annotation of them, understand list of classes (e.g. reference tree), which should be loaded (and in addition - how they should be loaded)
Load classes with different ways:
3.1. For classes, which are annotated with #ConfigurationProperties - put configuration items here
3.2. For classes, which are annotated with #RestController - register them as rest controllers
3.N. etc...
How does Spring understand, what configuration should be loaded?
Formally is it done by Spring Boot, however I will name it as Spring
Understand several initial configurations - they can be put into the class SpringApplicationBuilder, into the test annotations (see above), into the XML context, etc. For our case we use test annotation and #ContextConfiguration attribute
Recursive get all imported configuration (e.g. Spring reads #Import annotation, then it get children, then it check their imports, etc.)
Use Spring Factories to get configuration automatically from jar
Therefore, in our case, Spring will do actions like this:
Get configuration from test annotation
Get all other configurations by recursive way
Load all classes into the contet
Start test
Okay, it took me all day, but I finally got my application properties to load into my unit test context. I made 2 changes:
First, I added the #Service annotation to my VqsS3FileReader service - which I had originally forgotten. Also, while I had updated my Test to not inject the AmazonS3Config via the constructor, I had neglected to update my service to do the same. So I changed
this:
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
...
to this:
#Service
class VqsS3FileReader : VqsFileReader {
companion object: mu.KLogging()
#Resource
private lateinit var amazonS3Config: AmazonS3Config
...
Finally, I modified my Spring annotations on my test.
from this:
#ActiveProfiles("test")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
...
to this:
#ActiveProfiles("test")
#SpringBootTest
#ComponentScan("com.ilmn.*")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#EnableAutoConfiguration
#SpringJUnitConfig(SpringBootContextLoader::class)
class VqsS3FileReaderTest(): TestBase() {
...
It seems like I have an unordinary amount of annotations on my test now... so I will be looking carefully at what each of them really do, and see if I can reduce it. But at least my properties are being injected into my test context now.

Resources