Modularizing a Spring Boot Application
Modularizing a Spring Boot Application
Loading components from other sources
Every software project comes to a point where the code should be broken up into modules. These may be modules within a single code base or modules that each live in their own code base. This article explains some Spring Boot features that help to split up your Spring Boot application into several modules.
Code Example
This article is accompanied by a working code example on GitHub.
What’s a Module in Spring Boot?
A module in the sense of this article is a set of Spring components loaded into the application context.
A module can be a business module, providing some business services to the application or a technical module that provides cross-cutting concerns to several other modules or to the whole of the application.
The modules discussed in this article are part of the same monolithic codebase. To better enforce module boundaries, we could split up that monolithic codebase into multiple build modules with Maven or Gradle, if we so wish.
Options for Creating Modules
The base for a Spring Module is a @Configuration
-annotated class along the lines of Spring’s Java configuration feature.
There are several ways to define what beans should be loaded by such a configuration class.
@ComponentScan
The easiest way to create a module is using the @ComponentScan
annotation on a configuration class:
@Configuration
@ComponentScan(basePackages = "io.reflectoring.booking")
public class BookingModuleConfiguration {
}
If this configuration class is picked up by one of the importing mechanisms (explained later), it will look through all classes in the package io.reflectoring.booking
and load an instance of each class that is annotated with one of Spring’s stereotype annotations into the application context.
This way is fine as long as you always want to load all classes of a package and its sub-packages into the application context. If you need more control on what to load, read on.
@Bean
Definitions
Spring’s Java configuration feature also brings the @Bean
annotation for creating beans that are loaded into the application context:
@Configuration
public class BookingModuleConfiguration {
@Bean
public BookingService bookingService(){
return new BookingService();
}
// potentially more @Bean definitions ...
}
When this configuration class is imported, a BookingService
instance will be created and inserted into the application context.
Using this way to create a module gives a clearer picture of what beans are actually loaded, since you have a single place to look at - in contrast to using @ComponentScan
where you have to look at the stereotype annotations of all classes in the package to see what’s going on.
@ConditionalOn...
Annotations
If you need even more fine-grained control over which components should be loaded into the application context, you can make use of Spring Boot’s @ConditionalOn...
annotations:
@Configuration
@ConditionalOnProperty(name = "io.reflectoring.security.enabled",
havingValue = "true", matchIfMissing = true)
public class SecurityModuleConfiguration {
// @Bean definitions ...
}
Setting the property io.reflectoring.security.enabled
to false
will now disable this module completely.
There are other @ConditionalOn...
annotations you can use to define conditions for loading a module. These include a condition depending on the version of the JVM and the existence of a certain class in the classpath or a certain bean in the application context.
If you ever asked yourself how Spring Boot magically loads exactly the beans your application needs into the application context, this is how. Spring Boot itself makes heavy use of the @ConditionalOn...
annotations.
Options for Importing Modules
Having created a module, we need to import it into the application.
@Import
The most straight-forward way is to use the @Import
annotation:
@SpringBootApplication
@Import(BookingModuleConfiguration.class)
public class ModularApplication {
// ...
}
This will import the BookingModuleConfiguration
class and all beans that come with it - no matter whether they are declared by @ComponentScan
or @Bean
annotations.
@Enable...
Annotations
Spring Boot brings a set of annotations that each import a certain module by themselves. An example is @EnableScheduling
, which imports all Beans necessary for the scheduling sub system and its @Scheduled
annotation to work.
We can make use of this ourselves, by defining our own @EnableBookingModule
annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(BookingModuleConfiguration.class)
@Configuration
public @interface EnableBookingModule {
}
The annotation is used like this:
@SpringBootApplication
@EnableBookingModule
public class ModularApplication {
// ...
}
The @EnableBookingModule
annotation is actually just a wrapper around an @Import
annotation that imports our BookingModuleConfiguration
as before. However, if we have a module consisting of more than one configuration, this is a convenient and expressive way to aggregate these configurations into a single module.
Auto-Configuration
If we want to load a module automatically instead of hard-wiring the import into the source code, we can make use of Spring Boot’s auto-configuration feature.
To enable a module for auto configuration, put the file META-INF/spring.factories
into the classpath:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.reflectoring.security.SecurityModuleConfiguration
This would import the SecurityModuleConfiguration
class all its beans into the application context.
An auto-configuration is especially handy if we’re building a cross-cutting concern to be used in many Spring Boot applications. In this case, we can even build a separate starter module around the configuration.
Configuring a Module
With the @ConfigurationProperties
annotation, Spring Boot provides first-class support for binding external configuration parameters to a Spring bean in a type-safe manner.
When to use which Import Strategy?
This article presented the major options for creating and importing modules in a Spring Boot application. But when should we use which of those options?
Use @Import
for Business Modules
For modules that contain business logic - like the BookingModuleConfiguration
from the code snippets above - a static import with the @Import
annotation should suffice in most cases. It usually does not make sense to not load a business module, so we do not need any control about the conditions under which it is loaded.
Note that even if a module is always loaded, it still has a right to exist as a module, since it being a module enables it to live in its own package or even its own JAR file.
Use Auto-Configuration for Technical Modules
Technical modules, on the other hand - like the SecurityModuleConfiguration
from above - usually provide some cross-cutting concerns like logging, exception handling, authorization or monitoring features which the application can very well live without.
Especially during development, these features may not be desired at all, so we want to have a way to disable them.
Also, we do not want to import each technical module statically with @Import
, since they should not really have any impact on our code.
So, the best option for importing technical modules is the auto-configuration feature. The modules are loaded silently in the background and we can influence them outside of the code with properties.