1. Spring Boot vu depuis ASP.NET Core
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Exemple complet : code/spring-cloud-reactor/l01-mvc, une API de gammes sur Spring MVC. Ses tests comparent chaque réponse, extrait de journal et rapport d’erreur cité ci-dessous aux fichiers de expected, et le côté .NET est csharp/l01-di.cs.
La même application, deux frameworks
Section intitulée « La même application, deux frameworks »Une application ASP.NET Core dit ce qu’elle contient : Program.cs enregistre les services, lit la configuration et mappe les endpoints, une ligne à la fois. Une application Spring Boot, pour l’essentiel, ne le dit pas. Elle déclare une classe, en annote quelques autres, et le framework trouve le reste sur le class path. Les pièces sont les mêmes ; ce qui change, c’est qui écrit la liste.
| ASP.NET Core | Spring Boot | Rôle |
|---|---|---|
dotnet new webapi |
Spring Initializr | génère un projet |
Program.cs avec WebApplication.CreateBuilder |
une classe annotée @SpringBootApplication |
le point d’entrée |
IServiceCollection, IServiceProvider |
le conteneur IoC, un ApplicationContext |
crée et injecte les objets |
builder.Services.AddScoped<T>() |
le component scanning des classes @Component, ou des méthodes @Bean |
les enregistre |
AddControllers(), AddHealthChecks()… |
l’auto-configuration | active les fonctionnalités |
appsettings.json, appsettings.Development.json |
application.yml, application-dev.yml (profils) |
configuration |
IOptions<T> |
@ConfigurationProperties |
configuration typée |
[ApiController], minimal APIs |
@RestController (Spring MVC) |
endpoints |
| Kestrel | Tomcat (embarqué) | le serveur HTTP |
MapHealthChecks, IHealthCheck |
Actuator, HealthIndicator |
endpoints de santé et de gestion |
WebApplicationFactory<Program> |
@SpringBootTest |
tests d’intégration |
Générer un projet
Section intitulée « Générer un projet »Spring Initializr est à la fois une page web et une API HTTP. La page permet de cocher des dépendances ; l’API fait la même chose depuis un terminal :
curl.exe https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 ` -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales ` -d dependencies=web,actuator,validation -o scales.zipExpand-Archive scales.zip -DestinationPath scalescd scales.\mvnw.cmd packagecurl https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 \ -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales \ -d dependencies=web,actuator,validation -o scales.zipunzip scales.zip -d scalescd scales./mvnw packagecurl https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 \ -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales \ -d dependencies=web,actuator,validation -o scales.zipunzip scales.zip -d scalescd scales./mvnw packageJ’ai exécuté les commandes PowerShell, et les commandes Bash dans Git Bash, sous Windows le 2026-09-14 ; sous Linux et macOS, elles sont à vérifier (il faudra peut-être d’abord installer unzip). L’archive contient un projet Maven avec le Maven Wrapper (mvnw, qui télécharge Maven 3.9.16 à la première utilisation), une classe d’application et un test :
scales/├── .mvn/wrapper/maven-wrapper.properties├── mvnw, mvnw.cmd├── pom.xml└── src/ ├── main/java/dev/learn/scales/ScalesApplication.java ├── main/resources/application.properties └── test/java/dev/learn/scales/ScalesApplicationTests.javaLe pom.xml généré est court parce que son parent fait le travail, comme l’expliquait la leçon 10 du cours Java : spring-boot-starter-parent fixe la version de chaque bibliothèque et de chaque plugin Spring, comme Directory.Packages.props et les valeurs par défaut du SDK réunis.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> <!-- lookup parent from repository --></parent>...<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency></dependencies>Un starter est une dépendance sans code propre qui apporte un ensemble cohérent de bibliothèques : spring-boot-starter-webmvc apporte Spring MVC, Tomcat et Jackson. Il joue le rôle de la référence au framework qu’ajoute Microsoft.NET.Sdk.Web à un projet. Deux détails sont nouveaux dans Spring Boot 4, d’après le guide de migration : le starter web que je connaissais sous le nom spring-boot-starter-web s’appelle désormais spring-boot-starter-webmvc (l’API d’Initializr accepte encore l’ancien identifiant web, comme le montre la commande ci-dessus), et chaque technologie a son propre starter de test, nommé spring-boot-starter-<technology>-test.
Le POM parent du cours importe aussi le BOM de Spring Cloud, spring-cloud-dependencies 2025.1.3, le release train que spring.io associe à Spring Boot 4.0 et 4.1. Les leçons 1 à 4 n’utilisent pas encore Spring Cloud ; importer le BOM dès le départ permet aux leçons suivantes d’ajouter des dépendances sans version.
Le point d’entrée
Section intitulée « Le point d’entrée »// Program.cs : @SpringBootApplication active le component scanning et l'auto-configuration pour ce package et ses sous-packages.@SpringBootApplication@ConfigurationPropertiesScanpublic class ScalesApplication {
public static void main(String[] args) { SpringApplication.run(ScalesApplication.class, args); }}@SpringBootApplication combine trois annotations : @SpringBootConfiguration (cette classe peut déclarer des beans), @EnableAutoConfiguration (configurer ce que suggère le class path) et @ComponentScan (enregistrer chaque classe annotée de ce package et de ses sous-packages). @ConfigurationPropertiesScan ajoute les classes de configuration typée des mêmes packages. SpringApplication.run crée le conteneur, démarre le serveur embarqué et rend la main une fois l’application prête. Le journal montre les étapes ; le test qui le capture remplace l’identifiant du processus, le port, les chemins et les durées, qui changent d’une exécution à l’autre :
INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : Starting ScalesApplication using Java <version> with PID <pid> (<path>) INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : No active profile set, falling back to 1 default profile: "default" INFO <pid> --- [scales] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port <port> (http) INFO <pid> --- [scales] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] INFO <pid> --- [scales] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/<version>] INFO <pid> --- [scales] [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in <n> ms INFO <pid> --- [scales] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoints beneath base path '/actuator' INFO <pid> --- [scales] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port <port> (http) with context path '/' INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : Started ScalesApplication in <n> seconds (process running for <n>)[scales] est spring.application.name, et main est le thread. Sur ma machine, le démarrage complet a pris 1,5 seconde, pour l’essentiel du chargement de classes.
Les beans et le conteneur
Section intitulée « Les beans et le conteneur »Spring appelle beans les objets que gère son conteneur. Il y a deux façons d’en déclarer un, et une application Spring utilise les deux.
Le component scanning. Une classe annotée @Component, ou l’une de ses spécialisations @Service, @Repository, @Controller et @RestController, est enregistrée parce qu’elle se trouve dans un package scanné. Il n’y a de ligne AddSingleton nulle part :
// @Service est un @Component : le component scanning l'enregistre comme singleton, sans appel à AddSingleton.@Servicepublic class ScaleService {
private final NoteSpeller speller;
// Un constructeur unique n'a pas besoin de @Autowired : Spring injecte ses paramètres, comme le conteneur .NET. public ScaleService(NoteSpeller speller) { this.speller = speller; }Les méthodes @Bean. Une méthode d’une classe @Configuration renvoie l’objet à enregistrer. C’est la forme Spring de services.AddSingleton<INoteSpeller>(sp => ...), pour les objets qui demandent une décision ou qui viennent d’une bibliothèque qu’on ne peut pas annoter :
// Une classe @Configuration contient des méthodes de fabrique, comme services.AddSingleton<INoteSpeller>(sp => ...).@Configurationclass SpellingConfiguration {
@Bean NoteSpeller noteSpeller(ScalesProperties properties) { return switch (properties.spelling()) { case SHARPS -> NoteSpeller.sharps(); case FLATS -> NoteSpeller.flats(); }; }}Les paramètres d’une méthode @Bean sont injectés comme des paramètres de constructeur. NoteSpeller nomme une note avec des dièses (A#) ou des bémols (Bb) ; la section suivante lie ScalesProperties à la configuration.
Trois différences avec .NET comptent au quotidien :
- Singleton est la valeur par défaut. Un enregistrement .NET indique toujours une durée de vie. Un bean Spring est un singleton sauf s’il dit autre chose avec
@Scope:prototypeest le transient de .NET, etrequestest ce qui se rapproche le plus de scoped dans une application web. La plupart des beans d’un service Spring sont des singletons sans état. - Chaque bean a un nom.
scaleServicepour la classe ci-dessus,noteSpellerpour la méthode. Les noms comptent quand plusieurs beans partagent un type, et ils entrent en collision : à la leçon 4, une méthode@Beannommée comme sa propre classe@Configurationa empêché l’application de démarrer. - Le conteneur est vérifié au démarrage. Les singletons sont créés dès le démarrage du contexte, si bien qu’une dépendance manquante arrête l’application avant qu’elle ne serve une requête.
Quand le conteneur ne peut pas trancher
Section intitulée « Quand le conteneur ne peut pas trancher »Le cas d’une dépendance manquante montre le mieux la dernière différence. Le test StartupTest démarre un contexte qui enregistre ScaleService mais rien qui implémente NoteSpeller. Les failure analyzers de Spring Boot transforment l’exception en rapport :
***************************APPLICATION FAILED TO START***************************
Description:
Parameter 0 of constructor in dev.learn.scales.ScaleService required a bean of type 'dev.learn.scales.NoteSpeller' that could not be found.
Action:
Consider defining a bean of type 'dev.learn.scales.NoteSpeller' in your configuration.Le conteneur .NET construit un provider sans broncher et n’échoue qu’au moment où le service est résolu. ValidateOnBuild déplace la vérification au démarrage, ce que Spring fait toujours. Le côté C# affiche :
built a provider without INoteSpellerresolve: Unable to resolve service for type 'INoteSpeller' while attempting to activate 'ScaleService'.ValidateOnBuild: 1 error(s): Error while validating the service descriptor 'ServiceType: ScaleService Lifetime: Singleton ImplementationType: ScaleService': Unable to resolve service for type 'INoteSpeller' while attempting to activate 'ScaleService'.ServiceProviderOptions.ValidateOnBuild est activé par défaut dans l’environnement Development d’un hôte ASP.NET Core : les deux frameworks détectent donc ce cas au démarrage pendant le développement ; seul Spring le fait dans tous les environnements.
Deux candidats sont un cas plus délicat. Enregistrez deux beans NoteSpeller et demandez-en un :
// Deux beans NoteSpeller et un constructeur qui en demande un : .NET prendrait en silence le dernier enregistrement.@Configuration@Import(ScaleService.class)public class TwoSpellersApplication {
@Bean NoteSpeller sharpSpeller() { return NoteSpeller.sharps(); }
@Bean NoteSpeller flatSpeller() { return NoteSpeller.flats(); }}***************************APPLICATION FAILED TO START***************************
Description:
Parameter 0 of constructor in dev.learn.scales.ScaleService required a single bean, but 2 were found: - sharpSpeller: defined by method 'sharpSpeller' in dev.learn.broken.ambiguous.TwoSpellersApplication - flatSpeller: defined by method 'flatSpeller' in dev.learn.broken.ambiguous.TwoSpellersApplication
This may be due to missing parameter name information
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed.NET résout la même situation en silence, avec le dernier enregistrement :
two registrations, ScaleService uses FlatSpellerIEnumerable<INoteSpeller>: SharpSpeller, FlatSpellerkeyed "flats": FlatSpellerLes trois portes de sortie de Spring correspondent à celles de .NET :
| Spring | .NET |
|---|---|
@Primary sur un bean |
l’enregistrer en dernier |
un paramètre List<NoteSpeller> |
IEnumerable<INoteSpeller> |
@Qualifier("flatSpeller") sur le paramètre, ou une Map<String, NoteSpeller> indexée par nom de bean |
les keyed services, [FromKeyedServices("flats")] |
La ligne “This may be due to missing parameter name information” est ici du bruit : quand le nom du paramètre correspond à un nom de bean, Spring l’utilise comme qualificateur de repli, et il lui faut seulement -parameters, que le POM parent passe déjà à javac.
L’auto-configuration
Section intitulée « L’auto-configuration »Rien dans l’exemple ne crée de serveur Tomcat, de mapper JSON ni de DispatcherServlet (le contrôleur frontal de Spring MVC). Ce sont des classes d’auto-configuration, livrées dans les JAR de Spring Boot, qui les créent, chacune sous conditions. Démarrer l’application avec --debug affiche le rapport d’évaluation des conditions. Un extrait du mien :
============================CONDITIONS EVALUATION REPORT============================
Positive matches:-----------------
DispatcherServletAutoConfiguration matched: - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition)
JacksonAutoConfiguration#jacksonJsonMapper matched: - @ConditionalOnMissingBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) did not find any beans (OnBeanCondition)
TomcatServletWebServerAutoConfiguration matched: - @ConditionalOnClass found required classes 'jakarta.servlet.ServletRequest', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol', 'org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition)
Negative matches:-----------------
JacksonAutoConfiguration.XmlConfiguration: Did not match: - @ConditionalOnClass did not find required class 'tools.jackson.dataformat.xml.XmlMapper' (OnClassCondition)Deux conditions font l’essentiel du travail :
@ConditionalOnClass: configurer Tomcat parce que Tomcat est sur le class path. Ajouter un starter, c’est la façon d’activer une fonctionnalité, là où ASP.NET Core demande à la fois le package et un appelAdd…().@ConditionalOnMissingBean: créer unJsonMapperseulement si l’application n’en a pas déclaré. Déclarer son propre bean de ce type, c’est la façon de remplacer celui par défaut, sansReplaceniRemoveAllsur la collection de services.
Le package tools.jackson est lui aussi nouveau : Spring Boot 4 utilise Jackson 3, dont les packages ont quitté com.fasterxml.jackson.
La configuration
Section intitulée « La configuration »application.yml et les propriétés typées
Section intitulée « application.yml et les propriétés typées »spring: application: name: scales
# Lié à ScalesProperties ; SCALES_SPELLING=flats dans l'environnement le remplacescales: default-mode: ionian spelling: sharps
management: endpoints: web: exposure: include: health,info endpoint: health: show-details: alwaysapplication.properties (des lignes plates key=value) et application.yml sont équivalents ; Initializr génère le premier, beaucoup de projets utilisent le second. La section scales est liée à un record, l’équivalent Spring d’une classe d’options :
// Le pattern options : IOptions<ScalesOptions> lié à la section "Scales", validé au démarrage.@ConfigurationProperties("scales")@Validatedpublic record ScalesProperties(@NotBlank String defaultMode, @NotNull Spelling spelling) {
public enum Spelling { SHARPS, FLATS }}Le record est lui-même un bean : ScaleController et la méthode @Bean ci-dessus reçoivent directement ScalesProperties, là où le code .NET injecte IOptions<ScalesOptions> et lit .Value. @Validated avec des annotations Jakarta Bean Validation, c’est ValidateDataAnnotations().ValidateOnStart(). Un mode vide arrête l’application :
***************************APPLICATION FAILED TO START***************************
Description:
Binding to target dev.learn.scales.ScalesProperties failed:
Property: scales.defaultMode Value: "" Origin: "scales.default-mode" from property source "commandLineArgs" Reason: must not be blank
Action:
Update your application's configurationLe rapport nomme la source de la propriété, ici la ligne de commande. C’est le relaxed binding de Spring : scales.default-mode dans un fichier, scales.defaultMode en Java, et SCALES_DEFAULTMODE ou SCALES_DEFAULT_MODE dans l’environnement atteignent tous le même champ. La convention .NET pour la même chose est Scales__DefaultMode.
D’où vient une valeur
Section intitulée « D’où vient une valeur »Les deux frameworks empilent des sources de configuration, et une source ultérieure l’emporte. En .NET, l’ordre est celui des appels Add… ; le côté C# place une source en mémoire d’abord et les variables d’environnement ensuite, et affiche Scales:Spelling = flats, Scales:DefaultMode = ionian. Spring Boot a un ordre fixe d’une quinzaine de sources. Celles qu’on rencontre au quotidien, de la plus faible à la plus forte :
application.ymlempaqueté dans le JAR ;application-<profile>.ymlempaqueté dans le JAR ;- les mêmes fichiers à côté du JAR, en dehors ;
- les variables d’environnement ;
- les propriétés système Java (
-Dscales.spelling=flats) ; - les arguments de ligne de commande (
--scales.spelling=flats).
Les annotations de test se placent au-dessus de tout cela : @SpringBootTest(properties = ...) l’emporte sur les fichiers, ce que vérifie ProfilesTest.
Les profils
Section intitulée « Les profils »Un profil est un ensemble nommé de beans et de propriétés, activé avec spring.profiles.active. Il couvre ce que .NET répartit entre ASPNETCORE_ENVIRONMENT et appsettings.{Environment}.json, à ceci près que plusieurs profils peuvent être actifs à la fois et que leurs noms sont libres. application-flats.yml est chargé par-dessus application.yml quand le profil flats est actif :
# Chargé par-dessus application.yml quand le profil "flats" est actif, comme appsettings.Development.jsonscales: spelling: flats default-mode: aeolianLes beans peuvent aussi dépendre d’un profil. Ce runner n’existe qu’avec le profil print, ce qui donne un moyen rapide de voir la configuration sans démarrer de serveur web :
// S'exécute une fois le contexte démarré, seulement avec le profil "print" : un moyen rapide de voir la configuration à l'œuvre.@Component@Profile("print")class PrintScaleRunner implements ApplicationRunner {ApplicationRunner s’exécute une fois après le démarrage, comme le début d’un IHostedService qui fait son travail et rend la main. Lancer le JAR empaqueté avec une variable d’environnement :
$env:SCALES_SPELLING = 'flats'java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=noneRemove-Item Env:SCALES_SPELLINGSCALES_SPELLING=flats java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=noneSCALES_SPELLING=flats java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=nonespelling: FLATSScaleView[root=Bb, mode=ionian, notes=[Bb, C, D, Eb, F, G, A], chords=[Bb, Cm, Dm, Eb, F, Gm, Adim]]--spring.main.web-application-type=none démarre le conteneur sans Tomcat. La CI exécute la même commande via check.sh, avec le niveau de journalisation relevé à warn pour qu’il ne reste que ces lignes.
Le contrôleur
Section intitulée « Le contrôleur »// [ApiController] avec [Route("scales")] : la valeur renvoyée est écrite en JSON.@RestController@RequestMapping("/scales")class ScaleController {
private final ScaleService scales; private final ScalesProperties properties;
ScaleController(ScaleService scales, ScalesProperties properties) { this.scales = scales; this.properties = properties; }
// GET /scales/D?mode=dorian, comme [HttpGet("{root}")] avec un paramètre [FromQuery]. @GetMapping("/{root}") ScaleService.ScaleView scale(@PathVariable String root, @RequestParam Optional<String> mode) { return scales.describe(root, mode.orElse(properties.defaultMode())); }
// Un problème RFC 9457, comme Results.Problem(...) dans ASP.NET Core. @ExceptionHandler(IllegalArgumentException.class) ProblemDetail badName(IllegalArgumentException e) { ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); problem.setTitle("Unknown note or mode"); return problem; }}La correspondance est proche d’un contrôleur ASP.NET Core. @PathVariable et @RequestParam sont [FromRoute] et [FromQuery] ; un paramètre Optional est facultatif, comme un paramètre nullable. @ExceptionHandler intercepte une exception levée par les méthodes de ce contrôleur et renvoie un ProblemDetail, le corps RFC 9457 qu’ASP.NET Core écrit avec Results.Problem. HttpTranscriptTest démarre l’application sur un port libre et enregistre les réponses :
GET /scales/D?mode=dorian200 application/json{"root":"D","mode":"dorian","notes":["D","E","F","G","A","B","C"],"chords":["Dm","Em","F","G","Am","Bdim","C"]}
GET /scales/Bb200 application/json{"root":"A#","mode":"ionian","notes":["A#","C","D","D#","F","G","A"],"chords":["A#","Cm","Dm","D#","F","Gm","Adim"]}
GET /scales/H400 application/problem+json{"detail":"unknown note: H","instance":"/scales/H","status":400,"title":"Unknown note or mode"}Bb revient sous la forme A# : sans profil, le speller utilise des dièses. Le record est sérialisé avec les noms de ses composants, et Jackson écrit les propriétés du problème dans l’ordre alphabétique, là où ASP.NET Core commence par type et title.
Actuator
Section intitulée « Actuator »spring-boot-starter-actuator ajoute des endpoints de gestion sous /actuator. Seul health est exposé en HTTP par défaut ; la configuration ci-dessus ajoute info et affiche les détails. Une vérification personnalisée est un bean HealthIndicator, l’équivalent de IHealthCheck :
// IHealthCheck : Actuator ajoute chaque bean HealthIndicator à /actuator/health, sous le nom du bean.@Componentclass ScaleCatalogHealthIndicator implements HealthIndicator {
@Override public Health health() { int modes = Mode.values().length; return modes == 7 ? Health.up().withDetail("modes", modes).build() : Health.down().withDetail("modes", modes).build(); }}GET /actuator/health/scaleCatalog200 application/vnd.spring-boot.actuator.v3+json{"details":{"modes":7},"status":"UP"}
GET /actuator/health/liveness200 application/vnd.spring-boot.actuator.v3+json{"status":"UP"}L’indicateur s’appelle scaleCatalog, son nom de bean sans le suffixe HealthIndicator, et chaque composant a sa propre URL. Le /actuator/health complet listait aussi diskSpace, ping, ssl, livenessState et readinessState sur ma machine ; le test ne le cite pas, car l’espace disque et les chemins diffèrent d’une machine à l’autre. Les groupes liveness et readiness existent pour les sondes Kubernetes, comme les tags de health checks dans ASP.NET Core. HealthIndicator et Health ont migré dans Spring Boot 4 vers le package org.springframework.boot.health.contributor : les exemples plus anciens les importent donc depuis un package qui n’existe plus.
Tests d’intégration
Section intitulée « Tests d’intégration »@SpringBootTest démarre le contexte entier, comme WebApplicationFactory<Program>. Avec webEnvironment = RANDOM_PORT, il démarre aussi Tomcat sur un port libre, et @LocalServerPort injecte le port :
// WebApplicationFactory<Program> avec un vrai Kestrel : l'application démarre sur un port libre pour la classe de test.@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)class HttpTranscriptTest {
@LocalServerPort int port;Spring met le contexte en cache entre les classes de test qui utilisent la même configuration : huit classes de test ne démarrent donc pas huit serveurs. Le starter de test apporte JUnit, AssertJ et Mockito, que la leçon 11 du cours Java a couverts. Bien qu’aucun test ne mocke quoi que ce soit, le support de test de Spring initialise Mockito, et la JVM avertissait qu’un agent Java avait été chargé dynamiquement, jusqu’à ce que Surefire charge Mockito avec -javaagent, comme le fait cette leçon-là. L’exercice 3 écrit un test avec RestTestClient, le client fluide de Spring Framework 7 pour les tests.
À retenir
Section intitulée « À retenir »- Une application Spring Boot déclare moins de choses que
Program.cs: le component scanning enregistre les classes annotées, et l’auto-configuration active ce que contient le class path. - Les beans sont des singletons par défaut, ont un nom et sont créés au démarrage : une dépendance manquante ou ambiguë arrête donc l’application avec un rapport lisible. .NET prend le dernier enregistrement là où Spring refuse de deviner.
- Les méthodes
@Beand’une classe@Configurationsont les enregistrements par fabrique ;@Primary,@Qualifieret les paramètresListetMapdépartagent plusieurs candidats. @ConditionalOnClasset@ConditionalOnMissingBeanexpliquent l’essentiel de l’auto-configuration : ajouter un starter pour activer une fonctionnalité, déclarer son propre bean pour remplacer un défaut.--debugaffiche le rapport.application.yml, les profils et les records@ConfigurationPropertiesremplacentappsettings.json, les environnements etIOptions<T>. Les variables d’environnement l’emportent sur les fichiers empaquetés ; les arguments de ligne de commande l’emportent sur tout, sauf les tests.- Spring Boot 4 a renommé le starter web en
spring-boot-starter-webmvc, découpé les starters de test par technologie, adopté Jackson 3 et déplacé les classes de santé d’Actuator.
Exercices
Section intitulée « Exercices »- Avec le profil
flats,application-flats.ymlrègledefault-modesuraeolian. Prédisez ce qu’affiche le runnerprintavecSCALES_DEFAULT_MODE=doriandans l’environnement et--spring.profiles.active=print,flats, puis vérifiez.
Solution
$ SCALES_DEFAULT_MODE=dorian java -jar target/scales.jar --spring.profiles.active=print,flatsspelling: FLATSScaleView[root=Bb, mode=dorian, notes=[Bb, C, Db, Eb, F, G, Ab], chords=[Bbm, Cm, Db, Eb, Fm, Gdim, Ab]]L’orthographe des notes vient du fichier du profil, que rien ne remplace. Le mode vient de la variable d’environnement : les variables d’environnement passent avant tous les fichiers application*.yml empaquetés dans le JAR, spécifiques à un profil ou non. En .NET, le même résultat dépend de l’ordre des appels Add… ; l’hôte par défaut ajoute les variables d’environnement après les fichiers JSON, donc le résultat concorde. check.sh exécute cette commande en CI et compare la sortie.
- Les keyed services de .NET permettent à une classe de choisir une implémentation par clé à l’exécution. Enregistrez deux beans
NoteSpellernomméssharpsetflats, et écrivez unSpellingChooserqui les reçoit tous et nomme une note avec le speller dont on lui donne le nom, sans@Qualifier.
Solution
@Configurationstatic class Spellers {
@Bean NoteSpeller sharps() { return NoteSpeller.sharps(); }
@Bean NoteSpeller flats() { return NoteSpeller.flats(); }
@Bean SpellingChooser chooser(Map<String, NoteSpeller> byName, List<NoteSpeller> all) { return new SpellingChooser(byName, all); }}
// Map<String, T> est indexée par nom de bean, comme [FromKeyedServices("flats")] ; List<T> est IEnumerable<T>.record SpellingChooser(Map<String, NoteSpeller> byName, List<NoteSpeller> all) {
String name(Note note, String spelling) { NoteSpeller speller = byName.get(spelling); if (speller == null) { throw new IllegalArgumentException("unknown spelling: " + spelling + ", expected one of " + byName.keySet()); } return speller.name(note); }}Un paramètre Map<String, NoteSpeller> reçoit chaque bean de ce type, indexé par nom de bean, dans l’ordre d’enregistrement : le test vérifie que les clés sont [sharps, flats] et que A# devient Bb avec flats. Le test crée lui-même le conteneur avec new AnnotationConfigApplicationContext(Spellers.class), ce qui suffit au conteneur IoC ; Spring Boot n’intervient pas. @Qualifier("flats") fixerait un seul choix au moment de l’injection, comme [FromKeyedServices] ; la map le reporte à chaque appel, comme GetRequiredKeyedService.
- Écrivez un test d’intégration avec
RestTestClientqui vérifie queGET /scales/C?mode=bluesrépond 400 avec un corpsapplication/problem+jsondonttitle,detailetinstancesont ceux que fixe le contrôleur.
Solution
@SpringBootTest(classes = ScalesApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)@AutoConfigureRestTestClientclass Exercise3ProblemDetailsTest {
@Autowired RestTestClient client;
@Test void unknownModeIsABadRequestProblem() { client.get().uri("/scales/C?mode=blues") .exchange() .expectStatus().isBadRequest() .expectHeader().contentType(MediaType.APPLICATION_PROBLEM_JSON) .expectBody() .jsonPath("$.title").isEqualTo("Unknown note or mode") .jsonPath("$.detail").isEqualTo("unknown mode: blues") .jsonPath("$.instance").isEqualTo("/scales/C"); }}@AutoConfigureRestTestClient configure le client avec le port aléatoire : les URI sont donc relatives, comme avec le HttpClient que renvoie WebApplicationFactory.CreateClient(). classes = ScalesApplication.class est facultatif ici : sans lui, @SpringBootTest cherche une @SpringBootConfiguration dans le package du test et ses parents, et trouve ScalesApplication un niveau plus haut. instance est le chemin sans la query string.
- Référence Spring Boot : utiliser
@SpringBootApplication, auto-configuration, configuration externalisée, profils, endpoints d’Actuator, tester les applications Spring Boot - Guide de migration vers Spring Boot 4.0
- Référence Spring Framework : le conteneur IoC, la configuration en Java, les portées de beans, les réponses d’erreur
- .NET : l’injection de dépendances dans ASP.NET Core, la configuration, le pattern options, les environnements, les health checks, les tests d’intégration