Spring Boot is a huge success, perhaps even more so than its inceptors hoped for. There is a lot of documentation, blog posts, and presentations on Spring Boot. However, most of them are aimed toward a feature, like monitoring or configuring. Few - if any of them, describe real-world practices.
In particular, demos are mainly based on very simple apps, such as the Spring Pet Clinic. On the other hand, Spring legacy apps are usually designed into multiple modules. Not every app can, nor should, be designed as a micro-service. It doesn’t help that the Spring Initializr service doesn’t propose a multi-modules option.
In this post, I’d like to highlight how to design a Spring Boot having multiple modules. This an example of such design:
Let’s be honest, it’s not the best design ever. However, it’s the most widespread one regarding Spring projects. Thus it makes for a great example, and can be easily adapted to one’s own. |
Basic setup
- Create a parent folder e.g.
multiboot
- Create a POM with packaging
pom
in this folder. This will be the parent project. - Set its parent to the Spring Boot starter parent:multiboot/pom.xml
<project...> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> </project>
- Use the Spring Initializr (either from the website or from the IDE) to create different Spring Boot modules under the parent folder:
e.g.
repo
,service
andweb
. Set relevant dependencies for each of them.If using IntelliJ IDEA, this is quite straightforward with and then choosing Spring Initializr from the menu. - Add them as modules into the parent POM:multiboot/pom.xml
<project...> <modules> <module>repo</module> <module>service</module> <module>web</module> </modules> </project>
- This is the resulting structure:
Notice how it’s exactly the same as for legacy Spring projects structure - Create a Maven wrapper:
mvn -N io.takari:maven:wrapper
- Create a
.gitignore
with adequate data (or copy it from one of the module) - In the modules POM:
- Change the
parent
coordinates to the parent POM coordinates instead ofspring-boot-starter-parent
- Move section
properties
to the parent POM - Move section
build
to the parent POM, nestingplugins
intopluginManagement
- Optional: clean unnecessary tags, such as
packaging
(default isjar
),groupId
andversion
(inherited from parent),name
anddescription
.
- Change the
- What’s left in the modules POM should be quite concise, containing only
parent
,artifactId
anddependencies
:<project...> <modelVersion>4.0.0</modelVersion> <parent> <groupId>ch.frankel.blog.multiboot</groupId> <artifactId>parent</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>repo</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
- Remove application classes from
repo
andservice
modules as they are not entry-points. Create a configuration class in both of them. - In
service
, addrepo
as a dependency. Inweb
, addservice
. - Inject the
Repository
into theService
, and theService
into theController
That should be it!
The problem
At this point, the project seems to be configured adequately. However, launching the project will probably result in the following:
*************************** APPLICATION FAILED TO START *************************** Description: Parameter 0 of constructor in ch.frankel.blog.multiboot.web.PersonController required a bean of type 'ch.frankel.blog.multiboot.service.PersonService' that could not be found. Action: Consider defining a bean of type 'ch.frankel.blog.multiboot.service.PersonService' in your configuration.
The problem comes from the way component scanning is handled by Spring Boot.
Only the package where the @SpringBootApplication
-annotated class resides and its children will be scanned.
Chances are you designed a "classical" package structure with every module in its own package:
e.g. ch.frankel.blog.multiboot.web
, ch.frankel.blog.multiboot.service
and ch.frankel.blog.multiboot.repo
.
With the main application class in the first package, other packages won’t be scanned.
Hence, autowiring won’t take place and the above failure will happen.
The options
There are basically 2 options to resolve this problem:
- Move the main class
-
Obviously, moving the application class to the root package e.g.
ch.frankel.blog.multiboot
will solve the issue easily. However, it breaks the package structure design. - Configure the component scan location
-
The alternative is to tell Spring Boot in which locations it should scan. For regular beans, it’s quite straightforward:
@SpringBootApplication(scanBasePackageClasses = [ServiceConfig::class])
However, entities and JPA repositories require a dedicated annotation:
@EntityScan(basePackageClasses = [RepoConfig::class]) @EnableJpaRepositories(basePackageClasses = [RepoConfig::class])
Conclusion
Designing a multi-modules Spring Boot application requires a lot of manual setup compared to a standard Spring Boot app. But no more than a classical Spring app without Boot. Modules are a great way to enforce boundaries between chunks of not-so-related code. Plus, with Java 9, they can map to Java 9 modules. Finally, they can be a first step toward micro-services - or even render them unnecessary in one’s context.