In my first article, I would like to tell you how to deal with Spring Boot Configuration and Spring Boot Starters when you want to get your application more flexible due to the bootstrapping process.
Spring Boot provides a very useful tool that is used almost in every dependency of the project. If you look over the list of the dependencies Spring Boot project provides us, you will see a lot of them containing word “starter” in the name. This word means that the dependency provides a special way of configuration that is a part of the Spring Boot Framework functionality.
Spring Framework provides an ability to configure your beans using XML or Java Config way. The second one is the base of the Spring Boot’s starters mechanism. Every time your application is starting the framework looking over your base package and your project folders seeking @Configuration annotation to use it as a part of the initialization process. (You can learn much more about it in the official documentation). But it doesn’t find any configurations that are come with dependencies (Spring Framework ignores the configurations from the dependencies).
So, the starters mechanism is developed to get over that restriction and add the ability to configure beans just including dependencies. It may be very useful when you have a common library that provides some functionality that must be initialized and put into the DI container due to the application start process.
Creating a starter
The most common way to get your dependency configured is writing a Java Config that works on an application start. In most cases, a library user should just add your dependency and everything works automatically. To get it working you have to do two steps — writing your configuration and add it to the special file.
Let’s consider a simple example. We have a common library that provides some common code to its “clients” and we need to supply all the clients with RabbitMQ supporting by just adding this library to a module.
First, we have to write our configuration (I’m going to use Kotlin through whole the article):
As you can see, we defined some queue, exchange and bound them together.
If we add our dependency to any module now, we won’t get it working. As I said above, Spring doesn’t scan packages of dependencies seeking for configuration. But here is our way. We just have to create a file in the following path: resources/META-INF/spring.factories
That’s it. Everything works fine. Spring Boot finds the file and loads our configuration creating the beans we described inside.
Enabled or Disabled by a condition
What if we don’t need the configuration and it would be better to have a mechanism to disable our starter? Spring Boot supports a good way to get over such a situation and use “conditions” to determine when the configuration should be enabled.
ConditionOnMissingBean annotation means that the configuration should be applied only if the current state of an application bean container doesn’t have a bean with the passed type. Using that annotation, we can avoid the situations when we are trying to create duplicate beans.
Spring Boot supports much more conditional annotations and even provides an ability to write your own conditions: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html
Enabled by annotation
Another way to run your configuration is turning it on by a special annotation. There is nothing magical if you have already seen some annotations like EnableRabbit, EnableCache and etc.
This method is not supposed to use any conditions nor even starter declarations. Everything you need is just to use standard annotation — Import. This annotation allows you to import other configurations by the configuration you marked.
Now, if you mark any another configuration in your launching project, you will get included UserClientRabbitMQConfig during the starting process.
Let’s assume we have a microservice that provides business logic of users. Also, we have an endpoint application that should be able to communicate with that microservice and retrieve the data about users somehow.
First of all, we create a core library that contains our common DTOs and our interfaces.
After that, we decide to use RabbitMQ as a transport. According to our decision, we have to write an implementation of UserClient and configure some beans for queues and exchange creation.
This code implements default listener (with RabbitMQ) and configures beans needed for RabbitMQ client.
As you can see, all the classes are places in the core and this core is used by both applications (microservice and endpoint). But we don’t need UserClientRabbitMQConfig in the microservice and don’t need UserServerRabbitMQConfig in the endpoint. Furthermore, every server should implement its own version of UserListener. According to this approach, we will reach a case when both applications aren’t aware of RabbitMQ and everything they should know it’s two interfaces: UserClient and UserListener.
Let’s write a couple of annotations.
That’s all. Everything you need is just to mark your server with EnableUserServer annotation and mark your client with EnableUserClient annotation. Also, implement UserListener in the server. To make a call from the client to the server, you just need to autowire UserClient bean and use it as a usual service.
You can use a lot of ways to implement auto-configurable libraries for Spring Boot applications. Even more, you can mix them together to get more flexible configurations.