Qu’est-ce que Spring Boot ?
Spring Boot est une couche superposée au Spring Framework qui élimine la configuration autrefois requise par Spring. Au lieu de fichiers XML, de configurations de servlets et d’un conteneur de déploiement, vous écrivez une classe avec une méthode main, ajoutez une dépendance “starter”, et vous obtenez une application fonctionnelle avec un serveur embarqué.
Trois concepts clés assurent l’essentiel du travail. Les Starters regroupent les dépendances nécessaires à une fonctionnalité en une seule ligne. L’Auto-configuration inspecte le classpath et configure les beans à partir de valeurs par défaut cohérentes. Le serveur embarqué signifie que l’application est un processus Java classique — il n’y a rien à installer ou dans lequel déployer.
En conséquence, Spring Boot est devenu la méthode par défaut pour construire des services JVM, qu’il s’agisse de petites API internes ou de flottes de microservices complexes, tout en conservant toute la puissance de l’écosystème Spring en arrière-plan.
L’écosystème Spring en une minute
Spring n’est pas une simple bibliothèque, mais une famille de projets partageant un même modèle de programmation :
- Spring Framework — le conteneur central, l’injection de dépendances et la couche web.
- Spring Data — les repositories pour les bases de données SQL et NoSQL.
- Spring Security — l’authentification et l’autorisation.
- Spring Batch / Integration — le traitement par lots et la messagerie.
- Spring Cloud — la configuration, la découverte de services et la résilience pour les systèmes distribués.
Comme ils sont conçus ensemble, un projet peut les adopter progressivement sans modifier sa manière d’être écrit. Cette cohérence est la véritable raison pour laquelle les équipes standardisent leur développement sur Spring.
Starters et gestion des dépendances
Un starter est un ensemble optimisé de dépendances conçues pour fonctionner ensemble. Vous déclarez la fonctionnalité souhaitée, et non chaque jar individuellement.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
L’équivalent Gradle est tout aussi concis :
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-validation"
runtimeOnly "org.postgresql:postgresql"
}
Le POM parent ou le plugin de dependency-management fixe pour vous les versions compatibles. Ne surchargez une version que si vous en avez une raison précise, et laissez la plateforme gérer le reste.
La classe d’application et le serveur embarqué
Toute application Spring Boot commence ici. Cette unique classe suffit pour lancer un serveur web.
package com.example.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
@SpringBootApplication combine trois annotations : @Configuration, @EnableAutoConfiguration et @ComponentScan. Le scan des composants (component scanning) débute dans le package de cette classe et s’étend aux sous-packages, c’est pourquoi la classe principale doit se situer à la racine de votre structure de packages.
L’exécution de ./mvnw spring-boot:run lance un serveur Tomcat embarqué sur le port 8080. Vous pouvez le remplacer par Jetty ou Netty en modifiant simplement une dépendance, sans que le code de l’application n’en soit affecté.
La couche web annotée
Spring MVC mappe les requêtes HTTP vers des méthodes à l’aide d’annotations. @RestController combine @Controller et @ResponseBody, ainsi les objets retournés sont sérialisés en JSON par Jackson.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<PostResponse> list(
@RequestParam(defaultValue = "0") int page) {
return service.findPage(page);
}
@GetMapping("/{id}")
public PostResponse get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
return service.create(request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
@PathVariable lit depuis l’URL, @RequestParam depuis la query string, et @RequestBody désérialise le payload. Gardez vos contrôleurs légers : ils traduisent le HTTP en un appel vers un service, puis traduisent le résultat en retour. La logique métier appartient à la couche service.
L’injection de dépendances et le contexte d’application
Le contexte d’application est le conteneur qui crée les objets, appelés beans, et fournit leurs dépendances. Vous déclarez ce dont une classe a besoin ; Spring décide comment la construire.
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
public PostService(PostRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Transactional
public PostResponse create(CreatePostRequest request) {
var post = new Post(request.title(), request.body(), clock.instant());
return PostResponse.from(repository.save(post));
}
}
Les annotations de stéréotypes marquent les classes à gérer : @Component pour les beans génériques, @Service pour les services de domaine, @Repository pour l’accès aux données et @RestController pour les points de terminaison web. Pour tout ce que vous ne pouvez pas annoter — une classe tierce, un client configuré — déclarez-le dans une classe @Configuration.
@Configuration
class ClockConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
}
Privilégiez l’injection par constructeur partout. Cela rend les dépendances explicites, permet d’utiliser des champs final et vous permet d’instancier la classe dans un test unitaire simple sans avoir à démarrer Spring.
Spring Data JPA et la base de données
Les entités font correspondre les objets Java aux tables, et les repositories vous permettent d’accéder aux données sans avoir à écrire l’implémentation.
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Author author;
protected Post() {
}
public Post(String title, String body, Author author) {
this.title = title;
this.body = body;
this.author = author;
}
// getters and domain methods
}
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByPublishedTrueOrderByCreatedAtDesc();
@Query("select p from Post p join fetch p.author where p.id = :id")
Optional<Post> findWithAuthor(Long id);
}
Spring Data déduit la requête à partir du nom de la méthode, donc findByPublishedTrueOrderByCreatedAtDesc n’a pas besoin de corps. Utilisez @Query lorsque le nom dérivé deviendrait illisible, et join fetch pour éviter le problème de lazy-loading N+1. Maintenez ddl-auto à validate en production et gérez le schéma avec Flyway ou Liquibase.
Configuration avec application.yml et les profils
La configuration se trouve dans src/main/resources et peut être surchargée par des variables d’environnement, ce qui permet au même jar de s’exécuter partout.
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/blog
username: blog
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
server:
port: 8080
Les profils permettent à un build de se comporter différemment selon l’environnement.
# src/main/resources/application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
logging:
level:
root: warn
Activez un profil avec --spring.profiles.active=prod ou la variable d’environnement SPRING_PROFILES_ACTIVE. @ConfigurationProperties lie un groupe de paramètres à une classe typée, ce qui est plus sûr que de disperser des annotations @Value dans tout le code.
Validation
Les contraintes de Jakarta Bean Validation sur un objet de requête sont appliquées lorsque le paramètre du contrôleur est annoté avec @Valid.
public record CreatePostRequest(
@NotBlank @Size(max = 120) String title,
@NotBlank String body
) {
}
Un @RestControllerAdvice centralise la réponse d’erreur afin que chaque point de terminaison retourne le même format.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(first, second) -> first));
}
}
Validez aux frontières de votre application et permettez au domaine de faire confiance à ses entrées. Combiné à un gestionnaire d’exceptions cohérent, cela permet de supprimer les vérifications if (input == null) répétitives dans chaque service.
Spring Security en bref
Spring Security est une chaîne de filtres placée devant votre application. Dans les versions modernes de Spring Boot, on le configure via un bean plutôt que par XML.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
}
Ce bean unique exige un JWT valide pour chaque endpoint, à l’exception du health check. Des règles au niveau des méthodes avec @PreAuthorize permettent d’ajouter une autorisation granulaire là où elle est nécessaire. La sécurité est un sujet complexe — prévoyez de lire attentivement la documentation avant d’exposer quoi que ce soit sur internet.
Actuator : santé et métriques
L’ajout du starter Actuator expose gratuitement des points de terminaison opérationnels.
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
curl localhost:8080/actuator/health
/actuator/health s’intègre aux sondes de liveness et de readiness, et /actuator/metrics rapporte les statistiques de la JVM, de HTTP et de la source de données. N’exposez que ce dont vous avez besoin et sécurisez le reste — ces points de terminaison révèlent beaucoup d’informations sur un système en cours d’exécution.
Tests
@SpringBootTest démarre le contexte de l’application et sert de base aux tests d’intégration. MockMvc sollicite la couche web sans ouvrir de port réel.
@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listsPosts() throws Exception {
mockMvc.perform(get("/api/posts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Hello"));
}
}
Pour obtenir un retour plus rapide, limitez le contexte à la seule couche testée et simulez les collaborateurs (mock).
@WebMvcTest(PostController.class)
class PostControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private PostService service;
@Test
void returnsNotFound() throws Exception {
given(service.findById(1L)).willThrow(new PostNotFoundException(1L));
mockMvc.perform(get("/api/posts/1"))
.andExpect(status().isNotFound());
}
}
Utilisez des slices pour les contrôleurs et les dépôts, et réservez le contexte complet pour quelques tests de bout en bout. Testcontainers vous permet d’utiliser une véritable base de données dans vos tests sans avoir à simuler la persistance.
Construction et déploiement
Le build produit un “fat jar” exécutable qui contient vos classes, vos dépendances et le serveur embarqué.
./mvnw clean package
java -jar target/blog-0.0.1-SNAPSHOT.jar
Pour les conteneurs, Buildpacks crée une image optimisée sans nécessiter de Dockerfile.
./mvnw spring-boot:build-image
La configuration provient de l’environnement, ainsi le même artefact passe de l’étape de staging à la production sans modification. N’ajoutez un Dockerfile que lorsque vous avez besoin d’un contrôle que Buildpacks ne propose pas, et privilégiez un build multi-étape (multi-stage build) pour garder l’image légère.
Bonnes pratiques
- Organisez vos packages par fonctionnalité afin que chaque domaine possède son propre contrôleur, service et repository.
- Utilisez l’injection par constructeur et gardez vos champs
final. - Retournez des DTO depuis les contrôleurs plutôt que des entités JPA.
- Validez les objets de requête avec
@Validet gérez les erreurs dans un seul@RestControllerAdvice. - Gardez
ddl-autoàvalidateet gérez les modifications de schéma avec Flyway ou Liquibase. - Externalisez la configuration et utilisez des profils plutôt que de reconstruire l’application pour chaque environnement.
- Ajoutez Actuator, puis exposez et sécurisez uniquement les endpoints dont vous avez réellement besoin.
Erreurs courantes
- L’injection de champs avec
@Autowired, ce qui masque les dépendances et complique les tests. - La sérialisation directe des entités, entraînant des fuites de colonnes internes ou des problèmes de lazy-loading.
- Laisser la logique métier s’accumuler dans les contrôleurs au lieu de la déléguer aux services.
- L’utilisation de
ddl-auto: updateen production, créant un décalage avec le schéma réel. - Démarrer l’intégralité du contexte pour chaque test unitaire, ce qui ralentit la suite de tests.
- Exposer tous les points de terminaison Actuator sur l’internet public.
- Ajouter des starters inutiles, ce qui impacte les performances au démarrage.
Et après ?
Spring Boot enseigne l’injection de dépendances, l’auto-configuration et la conception en couches — des concepts que l’on retrouve bien au-delà de Java. Si vous appréciez le modèle basé sur les modules et l’injection, NestJS l’applique en TypeScript, tandis qu’ Express propose une alternative minimaliste. La couche web annotée est, au final, une REST API, et la plupart des applications persistent leurs données dans un stockage relationnel ; PostgreSQL est donc la suite logique.