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.|
- Create a parent folder e.g.
- Create a POM with packaging
pomin 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.
web. Set relevant dependencies for each of them.
If using IntelliJ IDEA, this is quite straightforward withand 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
.gitignorewith adequate data (or copy it from one of the module)
- In the modules POM:
- Change the
parentcoordinates to the parent POM coordinates instead of
- Move section
propertiesto the parent POM
- Move section
buildto the parent POM, nesting
- Optional: clean unnecessary tags, such as
version(inherited from parent),
- Change the
- What’s left in the modules POM should be quite concise, containing only
<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
servicemodules as they are not entry-points. Create a configuration class in both of them.
repoas a dependency. In
- Inject the
Service, and the
That should be it!
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.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.
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.multibootwill 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])
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.