Article original : How to Build Multi-Module Projects in Spring Boot for Scalable Microservices
À mesure que les applications logicielles deviennent plus complexes, la gestion de l'évolutivité, de la modularité et de la clarté devient essentielle.
La structure multi-modules de Spring Boot vous permet de gérer différentes parties de l'application indépendamment, ce qui permet à votre équipe de développer, tester et déployer des composants séparément. Cette structure maintient le code organisé et modulaire, ce qui est utile pour les microservices et les grands systèmes monolithiques.
Dans ce tutoriel, vous allez construire un projet Spring Boot multi-modules, chaque module étant dédié à une responsabilité spécifique. Vous apprendrez à configurer les modules, à configurer la communication inter-modules, à gérer les erreurs, à implémenter la sécurité basée sur JWT et à déployer en utilisant Docker.
Prérequis :
Connaissance de base de Spring Boot et Maven.
Familiarité avec Docker et les concepts CI/CD (optionnel mais utile).
Table des matières
1. Pourquoi des projets multi-modules ?
Dans les projets à module unique, les composants sont souvent étroitement couplés, ce qui rend difficile la mise à l'échelle et la gestion de bases de code complexes. Une structure multi-modules offre plusieurs avantages :
Modularité : Chaque module est dédié à une tâche spécifique, telle que la gestion des utilisateurs ou des stocks, simplifiant ainsi la gestion et le dépannage.
Évolutivité de l'équipe : Les équipes peuvent travailler indépendamment sur différents modules, minimisant les conflits et améliorant la productivité.
Déploiement flexible : Les modules peuvent être déployés ou mis à jour indépendamment, ce qui est particulièrement bénéfique pour les microservices ou les grandes applications avec de nombreuses fonctionnalités.
Exemple concret
Considérons une grande application de commerce électronique. Son architecture peut être divisée en modules distincts :
Gestion des clients : Responsable de la gestion des profils, des préférences et de l'authentification des clients.
Gestion des produits : Se concentre sur la gestion des détails des produits, des stocks et des prix.
Traitement des commandes : Gère les commandes, les paiements et le suivi des commandes.
Gestion des stocks : Supervise les niveaux de stock et les commandes des fournisseurs.
Étude de cas : Netflix
Pour illustrer ces avantages, examinons comment Netflix utilise une architecture multi-modules.
Netflix est un exemple phare d'une entreprise qui utilise efficacement cette approche grâce à son architecture de microservices. Chaque microservice chez Netflix est dédié à une fonction spécifique, telle que l'authentification des utilisateurs, les recommandations de contenu ou les services de streaming.
Cette structure modulaire permet à Netflix de mettre à l'échelle ses opérations de manière efficace, de déployer des mises à jour indépendamment et de maintenir une haute disponibilité et performance. En découplant les services, Netflix peut gérer des millions d'utilisateurs et diffuser du contenu de manière transparente dans le monde entier, assurant ainsi un système robuste et flexible qui soutient sa vaste et dynamique plateforme.
Cette architecture non seulement améliore l'évolutivité, mais aussi l'isolation des pannes, permettant à Netflix d'innover rapidement et de répondre efficacement aux demandes des utilisateurs.
2. Structure et architecture du projet
Revenons maintenant à notre projet d'exemple. Votre projet Spring Boot multi-modules utilisera cinq modules clés. Voici la structure :
codespring-boot-multi-module/
common/ # Utilitaires et constantes partagés
domain/ # Entités de domaine
repository/ # Couche d'accès aux données (DAL)
service/ # Logique métier
web/ # Application principale Spring Boot et contrôleurs
Chaque module a un rôle spécifique :
common: Stocke les utilitaires partagés, les constantes et les fichiers de configuration utilisés dans d'autres modules.domain: Contient les modèles de données de votre application.repository: Gère les opérations de base de données.service: Encapsule la logique métier.web: Définit les points de terminaison de l'API REST et sert de point d'entrée de l'application.
Cette structure est alignée avec les principes de séparation des préoccupations, où chaque couche est indépendante et gère sa propre logique.
Le diagramme ci-dessous illustre les différents modules :

3. Comment configurer le projet parent
Étape 1 : Créer le projet racine
Exécutons ces commandes pour créer le projet parent Maven :
mvn archetype:generate -DgroupId=com.example -DartifactId=spring-boot-multi-module -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd spring-boot-multi-module
Étape 2 : Configurer le pom.xml parent
Dans le pom.xml, définissons nos dépendances et modules :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://www.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-multi-module</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>domain</module>
<module>repository</module>
<module>service</module>
<module>web</module>
</modules>
<properties>
<java.version>11</java.version>
<spring.boot.version>2.5.4</spring.boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Ce fichier pom.xml centralise les dépendances et les configurations, facilitant ainsi la gestion des paramètres partagés entre les modules.
4. Comment créer les modules
Module Common
Créons un module common pour définir des utilitaires partagés comme les formatteurs de date. Créez ce module et ajoutez une classe d'utilitaire d'exemple :
mvn archetype:generate -DgroupId=com.example.common -DartifactId=common -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Utilitaire de formatage de date :
package com.example.common;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateUtils {
public static String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
Module Domain
Dans le module domain, vous définirez vos modèles de données.
package com.example.domain;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class User {
@Id
private Long id;
private String name;
// Getters et Setters
}
Module Repository
Créons le module repository pour gérer l'accès aux données. Voici une interface de dépôt de base :
package com.example.repository;
import com.example.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {}
Module Service
Créons le module service pour contenir votre logique métier. Voici un exemple de classe de service :
package com.example.service;
import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
Module Web
Le module web sert de couche d'API REST.
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
}
5. Communication inter-modules
Pour éviter les dépendances directes, vous pouvez utiliser des API REST ou des courtiers de messages (comme Kafka) pour la communication inter-modules. Cela garantit un couplage lâche et permet à chaque module de communiquer indépendamment.
Le diagramme ci-dessous montre comment les modules communiquent entre eux :

Le diagramme illustre comment différents composants du système communiquent pour traiter les requêtes efficacement.
Le module Web traite les requêtes API entrantes et les transmet au module Service, qui contient la logique métier. Le module Service interagit ensuite avec le module Repository pour récupérer ou mettre à jour les données dans la base de données. Cette approche en couches garantit que chaque module fonctionne indépendamment, favorisant la flexibilité et une maintenance plus facile.
Exemple utilisant Feign Client :
Dans le contexte de la communication inter-modules, l'utilisation d'outils comme Feign Clients est un moyen puissant d'atteindre un couplage lâche entre les services.
Le client Feign permet à un module de communiquer de manière transparente avec un autre via des appels d'API REST, sans nécessiter de dépendances directes. Cette approche s'intègre parfaitement dans l'architecture en couches décrite précédemment, où le module Service peut récupérer des données à partir d'autres services ou microservices en utilisant des clients Feign, plutôt que d'accéder directement aux bases de données ou de coder en dur des requêtes HTTP.
Cela simplifie non seulement le code, mais améliore également l'évolutivité et la maintenabilité en isolant les dépendances de service.
@FeignClient(name = "userServiceClient", url = "http://localhost:8081")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
6. Problèmes courants et solutions
Lors de la mise en œuvre d'une architecture multi-modules, vous pouvez rencontrer plusieurs défis. Voici quelques problèmes courants et leurs solutions :
Dépendances circulaires : Les modules peuvent dépendre involontairement les uns des autres, créant une dépendance circulaire qui complique les builds et les déploiements.
- Solution : Concevez soigneusement les interfaces des modules et utilisez des outils de gestion des dépendances pour détecter et résoudre les dépendances circulaires tôt dans le processus de développement.
Sur-ingénierie : Il existe un risque de créer trop de modules, entraînant une complexité inutile.
- Solution : Commencez avec un ensemble minimal de modules et ne les divisez davantage que lorsqu'il y a un besoin clair, en veillant à ce que chaque module ait une responsabilité distincte.
Configurations incohérentes : La gestion des configurations dans plusieurs modules peut entraîner des incohérences.
- Solution : Utilisez des outils de gestion de configuration centralisés, tels que Spring Cloud Config, pour maintenir la cohérence entre les modules.
Surcoût de communication : La communication inter-modules peut introduire de la latence et de la complexité.
- Solution : Optimisez la communication en utilisant des protocoles efficaces et envisagez la messagerie asynchrone lorsque cela est approprié pour réduire la latence.
Complexité des tests : Tester un projet multi-modules peut être plus complexe en raison des interactions entre les modules.
- Solution : Mettez en œuvre une stratégie de test robuste qui inclut des tests unitaires pour les modules individuels et des tests d'intégration pour les interactions inter-modules.
En étant conscient de ces pièges et en appliquant ces solutions, vous pouvez gérer efficacement les complexités d'une architecture multi-modules et assurer un processus de développement fluide.
7. Stratégie de test et configuration
Tester chaque module indépendamment et en tant qu'unité est crucial dans les configurations multi-modules.
Tests unitaires
Ici, nous utiliserons JUnit et Mockito pour effectuer des tests unitaires :
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetUserById() {
User user = new User();
user.setId(1L);
user.setName("John");
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(user));
User result = userService.getUserById(1L);
assertEquals("John", result.getName());
}
}
Tests d'intégration
Et nous utiliserons Testcontainers avec une base de données en mémoire pour les tests d'intégration :
@Testcontainers
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class UserServiceIntegrationTest {
@Container
private static PostgreSQLContainer<?> postgresqlContainer = new PostgreSQLContainer<>("postgres:latest");
@Autowired
private UserService userService;
@Test
public void testFindById() {
User user = userService.getUserById(1L);
assertNotNull(user);
}
}
8. Gestion des erreurs et journalisation
La gestion des erreurs et la journalisation assurent une application robuste et débogable.
Gestion des erreurs
Dans cette section, nous explorerons comment gérer les erreurs de manière élégante dans votre application Spring Boot en utilisant un gestionnaire d'exceptions global. En utilisant @ControllerAdvice, nous mettrons en place une manière centralisée de capturer et de répondre aux erreurs, gardant ainsi notre code propre et nos réponses cohérentes.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND);
}
}
Dans l'exemple de code ci-dessus, nous définissons un GlobalExceptionHandler qui capture toute UserNotFoundException et retourne un message convivial comme "User not found" avec un statut 404. Ainsi, vous n'avez pas à gérer cette exception dans chaque contrôleur—vous l'avez couverte en un seul endroit !

Maintenant, examinons le diagramme. Voici comment tout cela se déroule : lorsqu'un client envoie une requête à notre Module Web, si tout se passe bien, vous obtiendrez une réponse réussie. Mais si quelque chose ne va pas, comme un utilisateur non trouvé, l'erreur sera capturée par notre Gestionnaire d'erreurs global. Ce gestionnaire journalise le problème et retourne une réponse propre et structurée au client.
Cette approche garantit que les utilisateurs reçoivent des messages d'erreur clairs tout en gardant les détails internes de votre application cachés et sécurisés.
Journalisation
La journalisation structurée dans chaque module améliore la traçabilité et le débogage. Vous pouvez utiliser un système de journalisation centralisé comme Logback et inclure des identifiants de corrélation pour tracer les requêtes.
9. Sécurité et intégration JWT
Dans cette section, nous allons configurer les JSON Web Tokens (JWT) pour sécuriser nos points de terminaison et contrôler l'accès en fonction des rôles des utilisateurs. Nous allons configurer cela dans la classe SecurityConfig, ce qui nous aidera à appliquer qui peut accéder à quelles parties de notre application.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
Dans l'exemple de code ci-dessus, vous pouvez voir comment nous avons défini les règles d'accès :
Les points de terminaison
/admin/**sont restreints aux utilisateurs ayant le rôleADMIN.Les points de terminaison
/user/**peuvent être accessibles par les utilisateurs ayant le rôleUSERouADMIN.Toute autre requête nécessitera que l'utilisateur soit authentifié.
Ensuite, nous configurons notre application pour valider les jetons entrants en utilisant .oauth2ResourceServer().jwt();. Cela garantit que seules les requêtes avec un jeton valide peuvent accéder à nos points de terminaison sécurisés.

Maintenant, parcourons le diagramme. Lorsqu'un client envoie une requête pour accéder à une ressource, le Filtre de sécurité vérifie d'abord si le jeton JWT fourni est valide. Si le jeton est valide, la requête est transmise au Module de service pour récupérer ou traiter les données. Si ce n'est pas le cas, l'accès est refusé immédiatement et le client reçoit une réponse d'erreur.
Ce flux garantit que seuls les utilisateurs authentifiés peuvent accéder aux ressources sensibles, gardant ainsi notre application sécurisée.
10. Déploiement avec Docker et CI/CD
Dans cette section, nous allons containeriser chaque module en utilisant Docker pour rendre notre application plus facile à déployer et à exécuter de manière cohérente dans différents environnements. Nous allons également configurer un pipeline CI/CD en utilisant GitHub Actions (mais vous pouvez utiliser Jenkins si vous préférez). Automatiser ce processus garantit que toute modification que vous poussez est automatiquement construite, testée et déployée.
Étape 1 : Containerisation avec Docker
Nous commençons par créer un Dockerfile pour le Module Web :
FROM openjdk:11-jre-slim
COPY target/web-1.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Ici, nous utilisons une version légère de Java 11 pour garder notre image petite. Nous copions le fichier .jar compilé dans le conteneur et le configurons pour qu'il s'exécute lorsque le conteneur démarre.
Étape 2 : Utilisation de Docker Compose pour le déploiement multi-modules
Maintenant, nous allons utiliser un fichier Docker Compose pour orchestrer plusieurs modules ensemble :
version: '3'
services:
web:
build: ./web
ports:
- "8080:8080"
service:
build: ./service
ports:
- "8081:8081"
Avec cette configuration, nous pouvons exécuter à la fois le Module Web et le Module de Service en même temps, ce qui facilite le démarrage de l'ensemble de l'application avec une seule commande. Chaque service est construit séparément à partir de son propre répertoire, et nous exposons les ports nécessaires pour y accéder.
Exemple de CI/CD avec GitHub Actions
name: CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
- name: Build with Maven
run: mvn clean install
Ce pipeline se déclenche automatiquement chaque fois que vous poussez du nouveau code ou créez une pull request. Il extrait votre code, configure Java et exécute une construction Maven pour s'assurer que tout fonctionne correctement.
11. Bonnes pratiques et cas d'utilisation avancés
Les bonnes pratiques suivantes assurent la maintenabilité et l'évolutivité.
Bonnes pratiques
Éviter les dépendances circulaires : Assurez-vous que les modules n'ont pas de références circulaires pour éviter les problèmes de construction.
Séparer clairement les préoccupations : Chaque module doit se concentrer sur une responsabilité.
Configurations centralisées : Gérez les configurations de manière centralisée pour des configurations cohérentes.
Cas d'utilisation avancés
Messagerie asynchrone avec Kafka : Utilisez Kafka pour une communication découplée entre les services. Les modules peuvent publier et s'abonner à des événements de manière asynchrone.
Client REST avec Feign : Utilisez Feign pour appeler des services au sein des modules. Définissez une interface de client Feign pour la communication.
Mise en cache pour la performance : Utilisez Spring Cache dans le module de service pour optimiser la récupération des données.
Conclusion et points clés
Un projet Spring Boot multi-modules offre modularité, évolutivité et facilité de maintenance.
Dans ce tutoriel, vous avez appris à configurer des modules, gérer la communication inter-modules, gérer les erreurs, ajouter de la sécurité et déployer avec Docker.
En suivant les bonnes pratiques et en utilisant des techniques avancées comme la messagerie et la mise en cache, vous optimiserez davantage votre architecture multi-modules pour une utilisation en production.