Aller au contenu

Premier projet

Tu vas générer le squelette de todo-api, comprendre chaque fichier, le lancer et lui ajouter un premier endpoint GET /hello.

Dans ce chapitre

  • Générer un projet avec start.spring.io (site web ou curl)
  • Lire l'arborescence et le pom.xml d'un projet Spring Boot 4
  • Comprendre @SpringBootApplication et ce que fait SpringApplication.run
  • Lancer l'appli et lire ses logs de démarrage
  • Écrire un premier @RestController et le tester avec curl
  • Configurer le port, recharger avec DevTools, produire un jar exécutable

Générer le projet

Un projet Spring Boot ne s'écrit pas à la main : on le génère avec Spring Initializr, l'équivalent de cargo new, mais avec les dépendances choisies d'avance.

Sur start.spring.io, remplis :

Champ Valeur
Project / Language Maven / Java
Spring Boot 4.1.1
Group / Artifact xyz.dunkan / todo-api
Package name xyz.dunkan.todo
Packaging / Java Jar / 25 (le site propose 17 par défaut)

Puis ajoute les dépendances : Spring Web, Validation, Spring Data JPA, Flyway Migration, H2 Database, PostgreSQL Driver et Spring Boot Actuator. Clique sur Generate : tu obtiens un zip.

La même chose en une commande (le site expose une API) :

curl -s https://start.spring.io/starter.zip \
  -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 \
  -d groupId=xyz.dunkan -d artifactId=todo-api -d name=todo-api -d packageName=xyz.dunkan.todo \
  -d dependencies=web,validation,data-jpa,flyway,h2,postgresql,actuator \
  -o todo-api.zip
unzip todo-api.zip -d todo-api

On prend tout de suite toutes les dépendances de l'API finale : les chapitres suivants s'en serviront sans retoucher au pom.xml. Group est l'équivalent inversé d'un nom de domaine (il sert d'identifiant Maven), Package name est le paquet Java racine du code.

Ce qu'il y a dans le projet

todo-api/
├── pom.xml                              description du projet et dépendances
├── mvnw, mvnw.cmd                       le wrapper Maven (Linux/macOS, Windows)
├── .mvn/wrapper/maven-wrapper.properties  version de Maven à télécharger
├── HELP.md, .gitignore, .gitattributes  aide (à supprimer), fichiers Git
└── src/
    ├── main/
    │   ├── java/xyz/dunkan/todo/
    │   │   └── TodoApiApplication.java  classe principale, avec main
    │   └── resources/
    │       ├── application.properties   configuration
    │       ├── db/migration/            scripts SQL de Flyway (vide pour l'instant)
    │       ├── static/                  fichiers servis tels quels (inutile ici)
    │       └── templates/               gabarits HTML (inutile pour une API)
    └── test/java/xyz/dunkan/todo/
        └── TodoApiApplicationTests.java test « l'appli démarre »

C'est la disposition standard de Maven, vue dans Maven : le code dans src/main/java, les fichiers non Java (configuration, SQL) dans src/main/resources, les tests à part. Tout ce qui est dans resources finit dans le jar, à la racine du classpath.

Le pom.xml

Le pom.xml est le Cargo.toml du projet. Voici l'essentiel de celui qui a été généré :

pom.xml (extrait)
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId> <!-- (1)! -->
    <version>4.1.1</version>
    <relativePath/>
</parent>

<groupId>xyz.dunkan</groupId>
<artifactId>todo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
    <java.version>25</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc</artifactId> <!-- (2)! -->
    </dependency>
    <!-- ... data-jpa, validation, flyway, actuator, h2console ... -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope> <!-- (3)! -->
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc-test</artifactId> <!-- (4)! -->
        <scope>test</scope>
    </dependency>
    <!-- ... un starter de test par module ... -->
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId> <!-- (5)! -->
        </plugin>
    </plugins>
</build>
  1. Le parent fixe la version de centaines de bibliothèques compatibles entre elles. C'est pour ça qu'aucune dépendance n'indique de <version>.
  2. Un starter : Spring MVC + Tomcat embarqué + Jackson. Dans Spring Boot 4, il s'appelle -webmvc (c'était -web avant).
  3. runtime : le pilote H2 n'est pas visible à la compilation, seulement à l'exécution. Ton code ne dépend que de l'API JDBC, pas d'un pilote précis.
  4. Spring Boot 4 a découpé les outils de test : un starter de test par module (-webmvc-test, -data-jpa-test...). Chacun apporte JUnit, AssertJ et Mockito.
  5. Le plugin Spring Boot : il fournit ./mvnw spring-boot:run et transforme le jar en jar exécutable.

Sous le capot : le parent et les versions

Le parent hérite de spring-boot-dependencies, une BOM (Bill Of Materials) : un grand <dependencyManagement> qui dit par exemple « H2, c'est la version 2.4.240 ». Il configure aussi les plugins, dont le compilateur avec l'option -parameters (utile plus bas). Pour voir le classpath réel : ./mvnw dependency:tree.

La classe principale

src/main/java/xyz/dunkan/todo/TodoApiApplication.java
package xyz.dunkan.todo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TodoApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(TodoApiApplication.class, args);
    }
}

Un main d'une ligne. Tout le travail est caché derrière l'annotation et l'appel à run.

Ce que fait @SpringBootApplication

C'est une annotation composée : elle porte elle-même trois annotations.

@SpringBootConfiguration
Une variante de @Configuration : la classe peut déclarer des beans. Elle sert aussi de point de repère aux tests, qui cherchent cette classe pour savoir quoi démarrer.
@EnableAutoConfiguration
Active l'auto-configuration : Spring Boot charge sa liste de classes de configuration et applique celles dont les conditions sont remplies (« si telle classe est dans le classpath », « si aucun bean de ce type n'existe déjà », « si telle propriété vaut true »...).
@ComponentScan
Parcourt le paquet de la classe, ici xyz.dunkan.todo, et tous ses sous-paquets, à la recherche de classes annotées @Component, @Service, @RestController, @Repository...

Piège : une classe en dehors du paquet racine

Une classe placée dans xyz.dunkan.web (à côté de xyz.dunkan.todo, pas en dessous) n'est jamais trouvée par le scan. Pas d'erreur : ton contrôleur est simplement ignoré et ses routes répondent 404. Range toujours ton code sous le paquet de la classe principale.

Ce que fait SpringApplication.run

  1. Il détermine le type d'application : ici une application web à servlets, parce que Spring MVC est dans le classpath.
  2. Il prépare l'environnement : application.properties, variables d'environnement, arguments --cle=valeur de la ligne de commande.
  3. Il affiche la bannière et crée le contexte d'application (le conteneur).
  4. Il enregistre les définitions de beans : les tiens (scan), puis ceux des auto-configurations retenues.
  5. Il « rafraîchit » le contexte : crée tous les beans dans l'ordre de leurs dépendances. Tomcat est créé et démarré à cette étape.
  6. Il rend la main. main se termine, mais la JVM reste en vie grâce aux threads de Tomcat.

Lancer l'application

./mvnw spring-boot:run

Le wrapper télécharge Maven au premier lancement, compile, puis démarre l'appli. Voici les logs de démarrage, raccourcis (la date, le PID et le nom du thread sont retirés) :

 :: Spring Boot ::                (v4.1.1)

INFO xyz.dunkan.todo.TodoApiApplication  : Starting TodoApiApplication using Java 25.0.4.1 with PID ...
INFO xyz.dunkan.todo.TodoApiApplication  : No active profile set, falling back to 1 default profile: "default"
INFO o.s.boot.tomcat.TomcatWebServer     : Tomcat initialized with port 8080 (http)
INFO com.zaxxer.hikari.pool.HikariPool   : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:ea4f1368-5acf-4955-aece-4b57667e75a7 user=SA
WARN o.f.core.internal.command.DbValidate : No migrations found. Are your locations set up correctly?
INFO org.hibernate.orm.core              : HHH000001: Hibernate ORM core version 7.4.5.Final
WARN JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. ...
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.boot.tomcat.TomcatWebServer     : Tomcat started on port 8080 (http) with context path '/'
INFO xyz.dunkan.todo.TodoApiApplication  : Started TodoApiApplication in 5.843 seconds (process running for 6.66)

Une ligne de log complète ressemble à ceci : 2026-10-21T15:42:04.518Z INFO 3929495 --- [todo-api] [ main] xyz.dunkan.todo.TodoApiApplication : Starting .... On y lit la date, le niveau, le PID, le nom de l'appli (spring.application.name), le thread, la classe qui écrit (nom de paquet abrégé) et le message.

Ce qu'il faut remarquer : tu n'as écrit aucune configuration, et pourtant un pool de connexions (HikariCP) s'est ouvert sur une base H2 en mémoire au nom aléatoire, Flyway a cherché des migrations, Hibernate a démarré et Actuator expose un endpoint. C'est l'auto-configuration : les jars sont dans le classpath, donc Boot les a branchés. Les deux avertissements seront traités au chapitre Base de données (JPA). Arrête l'appli avec Ctrl+C : Tomcat s'arrête proprement (graceful shutdown), puis le pool de connexions.

Le rapport des conditions

Lance l'appli avec --debug (par exemple ./mvnw spring-boot:run -Dspring-boot.run.arguments=--debug) : Boot affiche un CONDITIONS EVALUATION REPORT qui liste chaque auto-configuration, appliquée (Positive matches, par exemple DataSourceAutoConfiguration matched: @ConditionalOnClass found required classes 'javax.sql.DataSource', ...) ou non (Negative matches), avec la raison.

Premier contrôleur

Crée un sous-paquet hello et cette classe :

src/main/java/xyz/dunkan/todo/hello/HelloController.java
package xyz.dunkan.todo.hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController // (1)!
public class HelloController {

    @GetMapping("/hello") // (2)!
    public String hello(@RequestParam(defaultValue = "le monde") String name) { // (3)!
        return "Bonjour " + name + " !";
    }
}
  1. Un bean (trouvé par le scan) dont les méthodes répondent à des requêtes HTTP. La valeur renvoyée est écrite directement dans le corps de la réponse : du texte pour une String, du JSON pour un objet.
  2. Cette méthode répond à GET /hello. Il existe aussi @PostMapping, @PatchMapping, @DeleteMapping...
  3. Lit le paramètre de requête ?name=... ; s'il est absent, name vaut "le monde".

Relance l'appli, puis dans un autre terminal :

$ curl -i http://localhost:8080/hello
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 18
Date: Wed, 21 Oct 2026 15:40:49 GMT

Bonjour le monde !
$ curl "http://localhost:8080/hello?name=Duncan"
Bonjour Duncan !

Tu n'as enregistré la route nulle part : le scan a trouvé la classe, et Spring MVC a lu ses annotations pour construire sa table de routage. Le même endpoint avec tes outils habituels, où route et serveur sont déclarés explicitement :

use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;

#[derive(Deserialize)]
struct Params {
    name: Option<String>,
}

async fn hello(Query(params): Query<Params>) -> String {
    let name = params.name.unwrap_or_else(|| "le monde".to_string());
    format!("Bonjour {name} !")
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/hello", get(hello));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()

@app.get("/hello", response_class=PlainTextResponse)
def hello(name: str = "le monde") -> str:
    return f"Bonjour {name} !"
# Serveur lancé à part : uvicorn main:app

Piège : le nom du paramètre vient de la compilation

@RequestParam String name ne précise pas le nom "name" : Spring le lit par réflexion sur le paramètre de la méthode. Or javac n'enregistre ces noms que si on compile avec l'option -parameters, que le parent Spring Boot active. Si tu compiles autrement (une configuration d'IDE qui ignore le pom.xml, par exemple), l'appel échoue en 500 avec : Name for argument of type [java.lang.String] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag. Compile avec Maven, ou écris @RequestParam("name").

application.properties

Le fichier généré ne contient que la première ligne ; ajoute la seconde pour changer le port :

src/main/resources/application.properties
spring.application.name=todo-api
server.port=9090

spring.application.name est le nom affiché entre crochets dans chaque ligne de log. Chaque module documente ses propriétés (server.* pour le serveur, spring.datasource.* pour la base...), et les valeurs par défaut sont choisies par l'auto-configuration. Une propriété peut aussi arriver par la ligne de commande ou par une variable d'environnement, ce qui évite de reconstruire le jar :

java -jar target/todo-api-0.0.1-SNAPSHOT.jar --server.port=9090
SERVER_PORT=9090 java -jar target/todo-api-0.0.1-SNAPSHOT.jar

SERVER_PORT est relié à server.port automatiquement (majuscules, _ à la place des .). En cas de conflit, la ligne de commande l'emporte sur la variable d'environnement, qui l'emporte sur application.properties : le fichier donne les valeurs par défaut, l'environnement de lancement les adapte. Tout cela est détaillé au chapitre Configuration.

DevTools (optionnel)

Relancer l'appli à chaque modification devient vite pénible. Le module DevTools redémarre l'appli tout seul dès que des classes compilées changent. Ajoute-le au pom.xml :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

Lance ./mvnw spring-boot:run, modifie HelloController, puis recompile (depuis ton IDE, ou ./mvnw compile dans un autre terminal). Les logs affichent Restarting due to 2 class path changes et la nouvelle version répond quelques secondes plus tard. DevTools active aussi des réglages de développement (le log l'annonce : Devtools property defaults active!), comme la console web de H2.

Sous le capot

Ce n'est pas du rechargement à chaud : DevTools utilise deux class loaders, un pour les jars (qui ne changent pas) et un pour tes classes. À chaque changement, il jette le second et redémarre le contexte Spring, ce qui est bien plus rapide qu'un démarrage complet de la JVM. Conséquence : une base H2 en mémoire repart vide à chaque redémarrage. DevTools n'est jamais inclus dans le jar final.

Le jar exécutable

$ ./mvnw package
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Building jar: .../todo-api/target/todo-api-0.0.1-SNAPSHOT.jar
[INFO] --- spring-boot:4.1.1:repackage (repackage) @ todo-api ---
[INFO] BUILD SUCCESS
$ java -jar target/todo-api-0.0.1-SNAPSHOT.jar

package lance d'abord les tests : le test généré TodoApiApplicationTests démarre tout le contexte et échoue si l'appli ne démarre pas. (./mvnw package -DskipTests pour les sauter.) Puis l'objectif repackage du plugin Spring Boot transforme le jar en fat jar : environ 56 Mo, avec tes classes dans BOOT-INF/classes et 84 jars de dépendances dans BOOT-INF/lib. Son META-INF/MANIFEST.MF indique Main-Class: org.springframework.boot.loader.launch.JarLauncher et Start-Class: xyz.dunkan.todo.TodoApiApplication : java -jar lance JarLauncher, qui sait charger des jars imbriqués (Java standard ne le sait pas), puis appelle ton main. Comme un binaire cargo build --release, c'est un seul fichier à copier sur un serveur ; à la différence de Rust, il lui faut une JVM pour tourner.

Exercices

1. Un paramètre dans le chemin. Ajoute GET /hello/{name} qui répond Bonjour <name> !, par exemple curl http://localhost:8080/hello/Ada. Indice : @PathVariable.

Solution
@GetMapping("/hello/{name}")
public String helloPath(@PathVariable String name) {
    return "Bonjour " + name + " !";
}

curl http://localhost:8080/hello/Duncan répond Bonjour Duncan !. {name} dans le chemin est relié au paramètre du même nom (là encore grâce à -parameters). Import : org.springframework.web.bind.annotation.PathVariable.

2. Du JSON sans effort. Ajoute GET /greeting?name=... qui renvoie un objet JSON avec deux champs : message (le texte de bienvenue) et length (sa longueur). Indice : renvoie un record.

Solution
record Greeting(String message, int length) {}

@GetMapping("/greeting")
public Greeting greeting(@RequestParam(defaultValue = "le monde") String name) {
    String message = "Bonjour " + name + " !";
    return new Greeting(message, message.length());
}
$ curl "http://localhost:8080/greeting?name=Duncan"
{"message":"Bonjour Duncan !","length":16}

Le record peut être déclaré dans la classe du contrôleur. Jackson, branché par l'auto-configuration, sérialise ses composants en JSON ; c'est l'équivalent d'un #[derive(Serialize)] que tu n'aurais pas eu à écrire.

3. Enquête. Sans DevTools, http://localhost:8080/h2-console répond 404 alors que H2 est dans le classpath. Utilise le rapport --debug pour trouver pourquoi la console n'est pas activée, et comment l'activer.

Solution

Dans la section Negative matches du rapport :

H2ConsoleAutoConfiguration:
   Did not match:
      - @ConditionalOnBooleanProperty (spring.h2.console.enabled=true) did not find property 'spring.h2.console.enabled' (OnPropertyCondition)
   Matched:
      - @ConditionalOnClass found required class 'org.h2.server.web.JakartaWebServlet' (OnClassCondition)

La classe de la console est bien là, mais l'auto-configuration exige aussi une propriété. Ajoute spring.h2.console.enabled=true dans application.properties (DevTools le fait pour toi en développement).

À retenir

  • On génère un projet Spring Boot avec start.spring.io (site ou curl), en choisissant des starters.
  • Le parent fixe les versions et configure les plugins ; Spring Boot 4 renomme le starter web en spring-boot-starter-webmvc.
  • @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan : seules les classes du paquet principal et de ses sous-paquets sont trouvées.
  • SpringApplication.run prépare l'environnement, crée le contexte, instancie les beans et démarre Tomcat.
  • Un @RestController avec @GetMapping suffit pour un endpoint : pas d'enregistrement de route ni de démarrage de serveur à écrire.
  • application.properties donne la configuration par défaut ; la ligne de commande et les variables d'environnement la surchargent.
  • ./mvnw spring-boot:run pour développer, ./mvnw package puis java -jar pour un fat jar autonome (il faut une JVM).