CodeGym /Cours /Spring Core /Spring Boot : ce qu'il automatise

Spring Boot : ce qu'il automatise

Spring Core
Niveau 25, Leçon 2
Disponible

1. Lancement honnête du pure Spring

Nous avons déjà traité les odeurs architecturales : annotation soup, hidden dependencies, service locator et mutable singleton ne se guérissent pas avec une nouvelle annotation et ne disparaissent pas d’eux-mêmes. Mais même une application pure Spring saine souffre d’une autre douleur — la routine répétitive d’assemblage. Le contexte doit être monté de la même manière, l’environnement préparé de façon identique, la configuration enregistrée pareillement. C’est cette routine que Spring Boot prend en charge.

Nous avons déjà le tableau pure Spring de référence, donc un bref rappel suffit ici — juste pour voir le delta entre le lancement manuel et l’automatisation Boot :

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowMain {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext()) {
            context.getEnvironment().setActiveProfiles("demo");
            context.register(AppConfig.class);
            context.refresh();

            context.getBean(ScenarioRunner.class).run();
        }
    }
}

Le sens de ce code n’est pas dans le rituel autour de refresh(). Le sens est que toutes les étapes sont explicites et de notre côté : nous assemblons nous-mêmes le démarrage, nous configurons l’environnement et nous sommes responsables de la fermeture correcte du contexte. Spring Core le fait honnêtement et de manière transparente. Mais plus il y a de projets, plus on souhaite que le démarrage standard, le branchement de la configuration et infrastructure-by-convention cessent d’être écrits à la main à chaque fois.

2. L’idée de Spring Boot : pilote automatique de la routine

Avec Spring Boot, il arrive souvent un piège psychologique étrange : une personne a l’impression d’être « passée à un autre Spring ». Comme s’il y avait Spring « normal », puis Spring « façon boutique ». En réalité, Boot n’est pas un nouveau conteneur ni un « super-Spring », mais une surcouche qui rend le démarrage et l’infrastructure standard plus standardisés et plus rapides.

Imaginez que Spring Core est le moteur et la boîte de vitesses. Il est puissant, flexible, honnête, mais exige que vous soyez vous-même le conducteur : enclencher les vitesses, appuyer sur l’embrayage et ne pas oublier de fermer le contexte (oui, le conducteur doit aussi savoir « fermer le contexte », sinon le moteur aura un triste @PreDestroy). Spring Boot dans cette analogie est une boîte automatique + pilote automatique pour les actions typiques : il ne change pas la physique du moteur, il supprime simplement les étapes répétitives et introduit des conventions.

La thèse clé qu’il faut « enfoncer dans le cerveau » doucement et sans marteau : Spring Boot n’annule pas ApplicationContext. Il crée toujours le contexte, enregistre toujours les BeanDefinition, lance toujours les post-processeurs, crée toujours les proxies, injecte toujours les dépendances. Simplement, vous écrivez moins de code redondant et plus de votre code applicatif.

Il est utile de garder en tête ce schéma simple des couches :

flowchart TD
    A[Votre code : domain/application/infrastructure] --> B[Spring Framework : ApplicationContext, beans, lifecycle, events, AOP]
    C[Spring Boot : conventions + démarrage + auto-configuration] --> B
    C --> A

Boot est une couche qui aide à assembler une application Spring « selon la norme », plutôt que d’inventer à chaque fois son mini-starter « comment nous lançons le contexte dans notre entreprise ».

Le code ci-dessous appartient déjà au runtime Boot. Pour cela, il faut des dépendances Boot et sa propre méthode de lancement, et ici il n’est nécessaire que pour voir : quelles étapes manuelles du démarrage pure Spring actuel se transforment en conventions, et quelles mécaniques conteneur restent les mêmes.

3. @SpringBootApplication comme cadre de démarrage

Lorsque nous voyons une application Boot pour la première fois, il y a généralement deux choses : une classe avec main() et l’annotation @SpringBootApplication. Un débutant a l’impression : « Wow, une seule annotation qui fait tout ». En réalité, elle fait une chose très compréhensible : elle empaquète le cadre de configuration de démarrage que nous assemblions explicitement et de manière verbeuse en pure Spring.

Conceptuellement, @SpringBootApplication est une « moissonneuse » qui combine plusieurs idées que vous connaissez du cours. En langage humain, cela signifie à peu près : « c’est une classe de configuration, scanne les composants à proximité et active le mécanisme d’auto-configuration ». Historiquement, elle est composée de plusieurs annotations (l’ensemble classique est @Configuration, @ComponentScan, @EnableAutoConfiguration), mais dans cette leçon, ce n’est pas l’archéologie des annotations qui importe, mais l’effet : moins de boilerplate, plus de conventions.

Comparons deux « squelettes » de démarrage.

Voici comment nous faisions le cadre de démarrage en pure Spring :

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
// On scanne les composants dans le package donné (et dans les sous-packages)
@ComponentScan("com.example.contextflow")
// On ajoute une configuration supplémentaire (généralement un contrôle explicite des beans)
@Import(AppConfig.class)
public class BootstrapConfig {
    // Important : la classe elle-même peut être vide — elle sert de "point d'ancrage" pour la configuration
}

Et voici à quoi ressemble une version Boot typique :

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// Cette annotation "assemble" le démarrage typique :
// - classe de configuration
// - component scanning à partir du package actuel
// - activation de l'auto-configuration selon les conditions
public class ContextFlowApplication {
}

Le principal avantage ici n’est même pas la « brièveté ». Le principal avantage est la prévisibilité. Par défaut, Boot scanne les composants à partir du package où se trouve votre classe d’application et dans les sous-packages. C’est pourquoi dans les projets Boot, on a l’habitude de placer ContextFlowApplication (ou l’équivalent) dans le package racine du projet, par exemple com.example.contextflow, pour que le scanning « voie » automatiquement domain, application, infrastructure, support et config.

Cela fait écho à ce que nous avons discuté à propos de la structure des packages et de @ComponentScan : le scanning est un outil puissant tant que vous gardez les limites en tête, sans activer « scanne tout le classpath et que le destin décide ».

4. SpringApplication et le démarrage standard

Dans Spring pur, nous créions un AnnotationConfigApplicationContext, enregistrions la configuration, configurions Environment, appelions refresh() et fermions le contexte. C’est correct, mais c’est vraiment de la routine — surtout si vous avez 10 services et que chaque classe de lancement est écrite « selon les goûts de chacun » (ensuite le QA dit : « pourquoi le profil de ce service vient de la variable d’environnement, et celui de celui-ci de l’argument en ligne de commande ? »).

SpringApplication dans Boot est un lanceur standard qui effectue pour vous un « démarrage correct » selon les conventions. De l’extérieur, cela semble presque ridiculement simple :

import org.springframework.boot.SpringApplication;

public class ContextFlowApplication {

    public static void main(String[] args) {
        // Point d'entrée standard de Boot :
        // à l'intérieur, seront créés Environment, ApplicationContext, appliqués les initializers, et refresh() sera appelé
        SpringApplication.run(ContextFlowApplication.class, args);
    }
}

Mais à l’intérieur, cela ressemble conceptuellement à notre lancement manuel, mais avec des étapes supplémentaires : créer Environment, rassembler les property sources, appliquer les profils, créer le ApplicationContext approprié (un pour une application console, un autre pour le web), enregistrer les classes de configuration, appliquer les initializers, appeler refresh(), attacher un shutdown hook, etc.

Pour vous permettre de voir plus facilement le lien avec le cours, vous pouvez imaginer (très simplifié) ce pseudo-code :

// PAS un vrai code source, mais un modèle mental :
// le but est de voir les étapes familières, pas de reproduire le code réel de Boot
Environment env = buildEnvironment(args);      // on rassemble les property sources + profils + arguments
ApplicationContext ctx = createContext(env);   // on choisit le type de contexte selon le type d'application

registerPrimarySources(ctx, ContextFlowApplication.class); // on enregistre les classes de configuration "principales"
applyInitializers(ctx, env);                    // on applique les initializers avant refresh()

ctx.refresh();   // ici vit le pipeline que vous connaissez du conteneur

return ctx;      // ensuite le contexte vit jusqu’à l’arrêt (et sera fermé via le hook)

Notez que le mot refresh() n’a disparu nulle part. Simplement, vous n’êtes plus obligé de “bien” préparer manuellement le contexte à chaque fois jusqu’à ce moment.

Style Boot de lancement d’un scénario dans une application console

Dans notre main pure Spring, nous faisions :

context.getBean(ScenarioRunner.class).run();

Dans le monde Boot, pour les applications console, on utilise souvent l’interface CommandLineRunner (ou un modèle similaire) pour « exécuter du code après le démarrage du contexte ». C’est très logique : le démarrage de l’application devient unitaire, et ce que vous voulez faire après le démarrage est défini comme un bean.

Exemple « à quoi cela ressemblerait » pour ContextFlow :

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
// C'est un bean ordinaire : Boot garantit simplement qu'il sera appelé APRÈS le démarrage du contexte
public class ContextFlowStartupRunner implements CommandLineRunner {

    private final ScenarioRunner scenarioRunner;

    public ContextFlowStartupRunner(ScenarioRunner scenarioRunner) {
        // L'injection par constructeur reste la même — c'est toujours du DI dans ApplicationContext
        this.scenarioRunner = scenarioRunner;
    }

    @Override
    public void run(String... args) {
        // Ici, il est pratique de lancer le "scénario de l'application" une fois que toutes les dépendances sont prêtes
        scenarioRunner.run();
    }
}

Il n’y a rien de « bootien » dans le mauvais sens ici. C’est le même principe que dans notre cours : dépendances via le constructeur, lancement du scénario via une classe séparée, point d’entrée clair. Boot propose simplement un crochet standard où placer « ce qu’il faut faire après le démarrage ».

5. Starters et BOM : gestion des dépendances

Quiconque a déjà sélectionné manuellement des dépendances pour une application Spring sait que cela ressemble à un jeu « assemble un puzzle dont la moitié des pièces vient d’un autre puzzle ». Dans notre cours, nous avons résolu cela proprement : nous avons fixé les versions de Spring Framework et utilisons Spring Boot BOM 4.0.3 uniquement comme alignement, pas comme runtime.

Spring Boot va plus loin et fait deux choses : il propose la BOM comme standard (pour que les versions soient toujours compatibles) et propose des starters comme ensembles types de dépendances pour des scénarios courants.

Un starter n’est ni une magie ni une « bibliothèque qui sait tout faire ». C’est plutôt une « boîte Lego » qui contient déjà les pièces nécessaires d’un ensemble. Par exemple, si vous voulez de l’AOP, vous n’avez pas besoin de vous rappeler à chaque fois qu’il faut spring-aop et aspectjweaver, et que les versions doivent correspondre au contexte. Dans le monde Boot, vous prenez généralement un starter pour l’AOP, et il apporte ce qu’il faut.

Pour comparer, il est pratique de voir cela dans un tableau (simplifié) :

Ce que vous voulez Pure Spring (souvent à la main) Approche Boot (souvent via un starter)
Conteneur de base
spring-context
,
spring-beans
,
spring-core
starter de base (comme « package par défaut »)
AOP spring-aop +
aspectjweaver
starter pour l’AOP
Test du contexte spring-test + JUnit Jupiter / AssertJ starter pour les tests

Et encore une fois une idée importante : les starters sont avant tout une commodité de gestion des dépendances, pas une « nouvelle façon de penser le DI ». Même dans une application Boot, vous écrivez toujours @Configuration, @Bean, @Service, des constructeurs, etc. Simplement, vous passez moins de temps à sélectionner des versions compatibles et vous attrapez moins de NoClassDefFoundError au démarrage parce qu’il manque un jar.

6. Auto-configuration : les beans d’infrastructure

La partie la plus « mystique » de Boot pour un débutant est l’auto-configuration. On dirait que Boot crée des beans « de nulle part », et l’application se met soudainement à fonctionner. Mais nous sommes déjà assez grands (après 25 jours de Spring Core) pour dire : « stop, où est le BeanDefinition ? où sont les conditions d’enregistrement ? qui a ajouté ça ? »

Et c’est là que se produit un moment magnifique : l’auto-configuration est en grande partie constituée de classes @Configuration ordinaires qui enregistrent des beans conditionnellement, en s’appuyant sur les mêmes idées que nous avons étudiées dans le thème des profils et de @Conditional. Boot fournit simplement une énorme bibliothèque de telles configurations et les branche automatiquement si elles correspondent à l’environnement.

« Back off » (céder la place à l’utilisateur) — le caractère principal de Boot

L’une des conventions les plus importantes de Boot : « si l’utilisateur a déjà déclaré un bean lui-même, ne le dérange pas ». Ce comportement est souvent appelé back off. Dans votre application, vous êtes l’architecte principal. Boot est un assistant qui propose des solutions typiques tant que vous n’avez pas dit « je vais le faire à ma manière ».

Exemple mental (pas un vrai code source de Boot, mais une idée très similaire) :

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
// Une telle classe dans Boot est généralement branchée automatiquement (dans le cadre d’un ensemble d’auto-configuration)
public class DefaultMessageSourceAutoConfig {

    @Bean
    public MessageSource messageSource() {
        // Idée de Boot : "proposer un défaut" si l’utilisateur n’a PAS défini son propre bean
        // Dans le vrai Boot, cela est presque toujours accompagné de conditions comme @ConditionalOnMissingBean
        return new ResourceBundleMessageSource();
    }
}

Dans le vrai Boot, c’est aussi généralement « conditionnel » selon l’environnement. Par exemple, « crée MessageSource si les classes nécessaires sont dans le classpath, si des fichiers bundle existent, si aucun bean utilisateur n’est défini ». Au niveau des idées, c’est le même @Conditional, mais il y a beaucoup de conditions et elles sont écrites à l’avance pour vous.

Lien direct avec ce que nous savons déjà du cours

Pour que l’auto-configuration ne semble pas magique, essayez à chaque fois de vous poser mentalement trois questions (ce n’est pas une liste à mémoriser bêtement, mais une habitude de pensée) :

Première question — « quel bean est apparu ? ». Deuxième — « sur quelle base a-t-il été enregistré ? ». Troisième — « puis-je le redéfinir avec mon propre @Bean et comment savoir qui gagne ? ».

Et vous comprenez soudainement que l’auto-configuration est la suite de notre discussion sur BeanDefinition, la phase d’enregistrement, @Conditional, les post-processeurs et les « beans d’infrastructure ». Simplement, avant vous les écriviez vous-même, et maintenant une partie d’entre eux arrive « dans le kit ».

Exemple avec notre projet ContextFlow

En pure Spring, nous organisions la configuration assez explicitement : property sources, conversion service, message source, event listeners, AOP. Dans Boot, beaucoup de choses s’activent par conventions. Par exemple, messages.properties (et les versions localisées) dans Boot a par défaut un nom standard et est automatiquement repris dans MessageSource, si vous n’avez pas défini le vôtre.

Autrement dit, vous pouvez toujours créer votre propre MessageSource, comme nous l’avons fait dans le cours, et Boot « s’effacera ». Mais si vous n’avez rien fait, Boot propose un défaut. Cela fait gagner du temps et réduit la quantité de « boilerplate obligatoire » dans chaque service.

7. Ce que Boot n’automatise pas

À ce stade, il est facile de céder à la tentation : « si Boot fait tant de choses, alors il fera aussi l’architecture ». Non. Et c’est une bonne nouvelle, car sinon n’importe quelle entreprise achèterait « Spring Boot Enterprise Architecture Starter », et l’architecte irait boire du thé pour le reste de sa vie (peut-être avec des biscuits, mais quand même).

Boot n’écrit pas le modèle du domaine à votre place, ne choisit pas les limites des modules, ne décide pas où les événements sont appropriés et où un appel direct est nécessaire. Il n’annule pas les anti-patrons dont nous avons parlé dans la leçon précédente : annotation soup ne se guérit pas avec Boot, mutable singleton ne disparaît pas avec Boot, et service locator via ApplicationContext reste une mauvaise idée même si vous avez un beau application.yml.

Le plus important : Boot n’annule pas la compréhension du DI. Il ne fait pas en sorte qu’on puisse oublier l’injection par constructeur et que tout devienne soudainement parfait. Boot facilite simplement le démarrage et l’infrastructure. Votre code vit toujours dans ApplicationContext, et les lois du conteneur s’appliquent toujours. Si vous avez accidentellement créé deux NotificationSender sans @Primary/@Qualifier, Boot ne dira pas « bon, d’accord » — vous obtiendrez la même NoUniqueBeanDefinitionException, seulement peut-être plus rapidement.

8. ContextFlow sur Spring Boot : expérience de pensée

Il est utile de terminer la leçon par une petite « traduction » de notre ContextFlow actuel dans le langage Boot — non pas pour réécrire le projet tout de suite, mais pour voir : ce qui change et ce qui reste le même.

Point d’entrée

C’était (pure Spring) :

// En pure Spring, nous gérons nous-mêmes le cycle de vie du contexte
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Et nous décidons nous-mêmes quoi lancer après le démarrage
    context.getBean(ScenarioRunner.class).run();
}

Deviendrait (Boot) :

import org.springframework.boot.SpringApplication;

public class ContextFlowApplication {
    public static void main(String[] args) {
        // Boot standardise le démarrage : environnement, contexte, refresh(), shutdown hook
        SpringApplication.run(ContextFlowApplication.class, args);
    }
}

Le sens : le démarrage est devenu standardisé, et vous n’avez plus besoin de vous rappeler à chaque fois « où est refresh, où est close ».

Où vit le lancement du scénario

C’était : dans main, manuellement getBean().

Deviendrait : un bean-runner séparé (crochet standard après le démarrage).

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class RunScenarioOnStartup implements CommandLineRunner {

    private final ScenarioRunner runner;

    public RunScenarioOnStartup(ScenarioRunner runner) {
        // Le DI reste le même : les dépendances viennent du conteneur via le constructeur
        this.runner = runner;
    }

    @Override
    public void run(String... args) {
        // Ce code s’exécutera après que le contexte a été complètement monté
        runner.run();
    }
}

Notez comme cela s’intègre proprement dans notre architecture : ScenarioRunner reste votre composant, de domaine par essence, et « quand lancer » devient une décision d’infrastructure.

Configuration et profils

En pure Spring, nous savons déjà faire des profils, des propriétés et un assemblage conditionnel. Dans Boot, tout cela ne disparaît pas. Les conventions changent : on utilise plus souvent des noms de fichiers de configuration standard, et Boot branche automatiquement plus de property sources. Mais le principe est le même : Environment, profils actifs, conditions d’enregistrement, « des beans différents pour dev/demo/test ».

Autrement dit, votre modèle mental du cours ne se « transporte » pas simplement — il devient l’explication de pourquoi Boot a agi ainsi et pas autrement.

Events, AOP, proxy, post-processors

Si vous craigniez que dans Boot tout devienne « différent » — non. @EventListener reste @EventListener. @Aspect reste @Aspect. Le modèle de proxy est le même. Les post-processeurs n’ont disparu nulle part (dans un certain sens, Boot s’appuie même plus activement sur eux). La différence est simplement qu’une partie de l’infrastructure est branchée automatiquement, et vous l’écrivez moins souvent à la main.

9. Erreurs typiques lorsqu’on parle de Spring Boot

Erreur n°1 : penser que Boot est un « conteneur différent » qui remplace ApplicationContext.
En pratique, cela conduit à ce qu’une personne cesse de comprendre où vivent les beans et qui les gère. En conséquence, tout comportement inattendu est déclaré « magique ». Le remède est simple : toujours se rappeler qu’à l’intérieur, ApplicationContext, BeanDefinition, le cycle de vie et les règles de wiring fonctionnent toujours.

Erreur n°2 : considérer que starter = auto-configuration, et confondre « dépendances » et « comportement ».
Un starter est un ensemble pratique de jars. L’auto-configuration est un ensemble de configurations qui enregistrent des beans selon des conditions. Si on mélange tout, on ne comprend plus pourquoi quelque chose est apparu : parce qu’on a ajouté une dépendance, ou parce qu’une configuration s’est activée.

Erreur n°3 : croire que Boot corrigera l’architecture à votre place.
Boot peut accélérer le démarrage d’un projet, mais il ne rendra pas vos services sans état, ne vous débarrassera pas du mutable singleton, n’interdira pas le service locator et ne rangera pas les packages par sens. Si un projet développe une annotation soup, cela arrive encore plus vite avec Boot, car il y a plus d’annotations autour, et la discipline nécessaire reste la même.

Erreur n°4 : lutter contre l’auto-configuration à coups de force brute au lieu de comprendre les conditions.
Le chemin le plus douloureux est : « quelque chose est apparu, donc je vais tout désactiver ». Cela finit généralement par désactiver l’utile avec le superflu. Il est bien plus sain de penser avec ce que nous avons appris : « quel bean a été créé ? », « pourquoi ? », « comment puis-je le redéfinir avec mon propre bean de manière prévisible ? ».

Erreur n°5 : considérer SpringApplication.run() comme une incantation magique qu’on peut invoquer n’importe comment et n’importe où.
Parfois, les gens commencent à lancer plusieurs contextes, créent un contexte à l’intérieur d’un contexte, ou appellent run() dans les tests n’importe comment. À l’intérieur, il y a toujours un conteneur Spring avec ses propres lois : un contexte — un ensemble de singleton par conteneur, ses propres profils et ses propres property sources. Si on oublie cela, les bugs deviennent très « philosophiques ».

1
Mission
Spring Core, niveau 25, leçon 2
Bloqué
Application console Boot minimale
Application console Boot minimale
1
Mission
Spring Core, niveau 25, leçon 2
Bloqué
Bean `@Bean` explicite dans une application Boot
Bean `@Bean` explicite dans une application Boot
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION