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.xmld'un projet Spring Boot 4 - Comprendre
@SpringBootApplicationet ce que faitSpringApplication.run - Lancer l'appli et lire ses logs de démarrage
- Écrire un premier
@RestControlleret le tester aveccurl - 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é :
<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>
- 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>. - Un starter : Spring MVC + Tomcat embarqué + Jackson. Dans Spring Boot 4, il s'appelle
-webmvc(c'était-webavant). 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.- 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. - Le plugin Spring Boot : il fournit
./mvnw spring-boot:runet 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¶
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¶
- Il détermine le type d'application : ici une application web à servlets, parce que Spring MVC est dans le classpath.
- Il prépare l'environnement :
application.properties, variables d'environnement, arguments--cle=valeurde la ligne de commande. - Il affiche la bannière et crée le contexte d'application (le conteneur).
- Il enregistre les définitions de beans : les tiens (scan), puis ceux des auto-configurations retenues.
- 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.
- Il rend la main.
mainse termine, mais la JVM reste en vie grâce aux threads de Tomcat.
Lancer l'application¶
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 :
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 + " !";
}
}
- 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. - Cette méthode répond à
GET /hello. Il existe aussi@PostMapping,@PatchMapping,@DeleteMapping... - Lit le paramètre de requête
?name=...; s'il est absent,namevaut"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();
}
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 :
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());
}
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.runprépare l'environnement, crée le contexte, instancie les beans et démarre Tomcat.- Un
@RestControlleravec@GetMappingsuffit pour un endpoint : pas d'enregistrement de route ni de démarrage de serveur à écrire. application.propertiesdonne la configuration par défaut ; la ligne de commande et les variables d'environnement la surchargent../mvnw spring-boot:runpour développer,./mvnw packagepuisjava -jarpour un fat jar autonome (il faut une JVM).