CodeGym /Cours /Spring Core /Templates et en‑têtes en tant que ressources

Templates et en‑têtes en tant que ressources

Spring Core
Niveau 15, Leçon 3
Disponible

1. Architecture des templates et en‑têtes

Quand le projet est petit, on a très envie de traiter le template comme quelque chose de secondaire : « du texte et du texte, je vais le mettre quelque part et le lire plus tard ». C’est une étape normale, comme « je vais d’abord tout faire dans une classe, puis je refactorise » (et ensuite… jamais). Mais dans ContextFlow, les templates de notifications et les en‑têtes de rapports font partie du comportement de l’application, simplement exprimée non pas en Java mais dans des fichiers texte.

L’idée clé : une ressource doit avoir un emplacement clair dans le projet et un nom clair, sinon elle cesse d’être une « ressource applicative » et devient un « fichier aléatoire à côté du code ». Il est également important que le chemin vers la ressource ne soit pas dupliqué dans tout le code. Le chemin est aussi une connaissance de configuration. S’il est dispersé dans 5 classes, « renommer le fichier » devient un mini‑quête dont le boss principal est votre propre Ctrl+Shift+F.

Nous avons déjà un moyen d’obtenir Resource. Maintenant, nous devons fixer quels fichiers ContextFlow possède, comment les nommer et où les placer, pour que le projet ne se disperse pas en template1.txt et en chaînes de chemins aléatoires.

Carte des ressources de ContextFlow

Pour stocker les ressources proprement, il est utile de les nommer du point de vue du scénario, et non « au hasard ». Dans ContextFlow, à ce stade, nous avons besoin de trois artefacts textuels d’entrée : le template de notification de création de commande, le template de notification d’annulation de commande, et l’en‑tête pour le rapport quotidien. Même si vous n’avez pas encore créé un « vrai » moteur de template, ces textes ont déjà un sens métier.

Il est pratique de convenir dès maintenant que les noms de fichiers reflètent le scénario. C’est bien plus clair que template1.txt, car « template1 » ne dit presque rien, alors que order-created a déjà du sens.

Ci‑dessous, une « carte des ressources » minimale (non pas comme une bureaucratie, mais comme une aide‑mémoire pour le cerveau) :

Scénario dans l’application Ressource (fichier) Utilisation
Création de commande templates/notifications/order-created.txt Texte de notification au client/opérateur
Annulation de commande templates/notifications/order-cancelled.txt Texte de notification d’annulation
Génération de rapport templates/reports/daily-report-header.txt En‑tête du rapport

Pour l’instant, nous ne touchons pas à la localisation et à MessageSource (c’est une autre histoire), donc nous faisons une version de chaque ressource. Quand de nouvelles mécaniques s’ajoutent, la structure s’étend, mais il vaut mieux commencer simple et stable.

2. Structure de src/main/resources

Quand vous mettez un template « n’importe où », vous créez un problème pour vous‑même à l’avenir. Le bon endroit pour les ressources d’entrée est src/main/resources. Cela signifie qu’elles seront dans le classpath et voyageront avec votre application (dans le jar).

Nous avons déjà expliqué pourquoi à l’exécution, il ne s’agit pas du dossier src/main/resources mais du classpath. Pour cette leçon, une seule règle suffit : placer les fichiers d’entrée dans src/main/resources, et ensuite dans le code, y accéder comme des ressources classpath.

La structure minimale proposée pour aujourd’hui est la suivante :

src/main/resources
├─ contextflow.properties
└─ templates
   ├─ notifications
   │  ├─ order-created.txt
   │  └─ order-cancelled.txt
   └─ reports
      └─ daily-report-header.txt

Il est utile de garder à l’esprit la frontière : src/main/resources est ce que l’application lit en entrée. Et tout ce que l’application génère (rapports, fichiers d’audit), nous le plaçons dans build/ selon les règles du cours. Non pas parce que « Gradle le veut », mais parce que c’est une discipline de reproductibilité : vous pouvez toujours supprimer build/ sans perdre les « connaissances sources » de l’application, seulement les résultats de son travail.

Si l’on veut un peu d’auto‑dérision, on peut imaginer que resources est un « livre de recettes » et build/ est une « cuisine sale après la cuisson ». Il ne faut pas imprimer la recette sur la poêle.

3. Templates : noms et contenu

Quand vous inventez un format de template, il est très facile d’inventer accidentellement un mini‑langage de programmation. Et ensuite de le supporter héroïquement. Au niveau actuel, l’important est autre : que le texte soit lisible et que les placeholders soient évidents. Même si plus tard vous ferez la substitution avec un simple String#replace, votre futur code bénéficiera d’un format prévisible.

Pour ne pas traîner un langage de template séparé, prenons le contrat le plus simple : du texte normal plus des placeholders positionnels %s, qui peuvent ensuite être substitués via String.formatted(...). Pour nos notifications, c’est plus que suffisant.

Un exemple de contenu de order-created.txt pourrait être très simple :

ORDER CREATED: id=%s, customer=%s

Et order-cancelled.txt :

ORDER CANCELLED: id=%s, reason=%s

Et l’en‑tête du rapport daily-report-header.txt :

DAILY REPORT — ContextFlow
=========================

Notez : nous ne discutons pas encore de « comment formater correctement l’argent », « comment décliner les mots » et « comment prendre en compte la locale ». Notre objectif actuel est de rendre la ressource significative et stable. Toute complexité inutile revient à installer Kubernetes sur une application de trois classes : ça a l’air sérieux, mais ça nuit à l’apprentissage.

4. Centralisation des chemins vers les ressources

La douleur pratique la plus fréquente avec les ressources n’est pas la lecture, mais le banal « où se trouve ce fichier ? ». Si le chemin vers order-created.txt est écrit en chaîne à trois endroits, vous aurez à coup sûr une divergence : une classe cherchera order-created.txt, la deuxième order_create.txt, la troisième dans un dossier complètement différent. Et voilà que l’application « fonctionne parfois », ce qui est particulièrement agréable… lors d’une démo.

C’est pourquoi nous introduisons une règle simple : les chaînes de chemins vivent au même endroit. Cela peut être une classe avec des constantes, une configuration, ou des properties — mais pas « un peu partout ».

La variante minimale est une classe utilitaire de constantes :

package com.example.contextflow.infrastructure.resources;

public final class TemplatePaths {

    // Chemin vers le template de notification de création de commande (ressource classpath).
    public static final String ORDER_CREATED = "templates/notifications/order-created.txt";

    // Chemin vers le template de notification d'annulation de commande (ressource classpath).
    public static final String ORDER_CANCELLED = "templates/notifications/order-cancelled.txt";

    // Chemin vers l'en-tête du rapport quotidien (ressource classpath).
    public static final String DAILY_REPORT_HEADER = "templates/reports/daily-report-header.txt";

    // Nous interdisons la création d'instances : c'est un conteneur pour constantes, pas un 'objet avec état'.
    private TemplatePaths() {
    }
}

Il n’y a aucune magie ici : nous avons simplement retiré les « chaînes magiques » des classes métier. Cela donne déjà deux effets. Premièrement, l’IDE commence à aider : renommer une constante n’est pas la même chose que chercher dans le projet. Deuxièmement, le chemin passe d’une « chaîne aléatoire » à une « partie cohérente du projet ».

5. Ressources en tant que dépendances Spring

Si nous considérons les templates comme faisant partie de l’application, il est logique de franchir l’étape suivante : laissons le conteneur Spring participer à cette histoire. Ici, nous avons besoin d’un registre de ressources soigné : les classes ci‑dessous ne s’occupent pour l’instant que du regroupement et de la sélection de Resource, sans I/O et sans rendu de texte.

Une astuce pratique pour le débutant est de « grouper » les ressources dans un petit objet. Ainsi, nous n’avons pas besoin de multiplier une dizaine de beans Resource différents et de les gérer ensuite avec @Qualifier. Par exemple, créons un conteneur immuable pour les templates de notification :

package com.example.contextflow.infrastructure.resources;

import org.springframework.core.io.Resource;

// Nous groupons les ressources de notification dans un seul objet,
// pour ne pas disperser dans le code de multiples dépendances Resource individuelles.
public record NotificationTemplates(Resource orderCreated, Resource orderCancelled) {
}

Maintenant, dans la configuration, on peut assembler cet objet à partir de ClassPathResource. Important : ClassPathResource prend le chemin sans préfixe classpath: — car nous disons explicitement « c’est du classpath » par le type de ressource lui‑même.

import com.example.contextflow.infrastructure.resources.NotificationTemplates;
import com.example.contextflow.infrastructure.resources.TemplatePaths;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

@Bean
NotificationTemplates notificationTemplates() {
    // Nous créons les ressources strictement à partir du classpath,
    // afin que les templates voyagent avec l'application dans le jar.
    // Nous prenons les chemins d'un seul endroit (TemplatePaths),
    // pour ne pas multiplier les 'chaînes magiques'.
    return new NotificationTemplates(
            new ClassPathResource(TemplatePaths.ORDER_CREATED),
            new ClassPathResource(TemplatePaths.ORDER_CANCELLED));
}

Ensuite, n’importe quel bean d’infrastructure peut obtenir ces templates via son constructeur. Par exemple, notre NotificationTemplateRegistry conserve pour l’instant simplement des références aux ressources et sélectionne celle qui convient selon le scénario :

package com.example.contextflow.infrastructure.resources;

import org.springframework.stereotype.Component;

@Component
public class NotificationTemplateRegistry {

    // Le registre sait 'quels templates existent', mais ne fait pas encore d'I/O.
    private final NotificationTemplates templates;

    public NotificationTemplateRegistry(NotificationTemplates templates) {
        // DI : nous recevons l'ensemble prêt de ressources du conteneur.
        this.templates = templates;
    }
}

C’est bien un registre de références vers des ressources : la lecture du contenu n’est volontairement pas faite ici.

Si vous devez sélectionner une ressource selon le scénario, on peut aussi l’exprimer par une méthode simple, sans lire le contenu :

import com.example.contextflow.domain.model.OrderStatus;
import org.springframework.core.io.Resource;

public Resource templateFor(OrderStatus status) {
    // Nous sélectionnons le template par sa signification (statut de la commande),
    // et non par le chemin ou par 'où se trouve le fichier'.
    return status == OrderStatus.CANCELLED
            ? templates.orderCancelled()
            : templates.orderCreated();
}

On peut faire de même pour les rapports. Par exemple, créer un ReportTemplates séparé avec l’en‑tête :

package com.example.contextflow.infrastructure.resources;

import org.springframework.core.io.Resource;

// Un ensemble séparé de ressources pour les rapports : pour l'instant, seulement l'en-tête.
public record ReportTemplates(Resource dailyReportHeader) {
}

Et l’enregistrer dans la configuration :

import com.example.contextflow.infrastructure.resources.ReportTemplates;
import com.example.contextflow.infrastructure.resources.TemplatePaths;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

@Bean
ReportTemplates reportTemplates() {
    // Nous considérons également l'en-tête du rapport comme une ressource d'entrée (classpath).
    return new ReportTemplates(new ClassPathResource(TemplatePaths.DAILY_REPORT_HEADER));
}

Notez un point pédagogique important : nous ne lisons pas encore le texte, n’ouvrons pas de flux et ne traitons pas les erreurs I/O. La tâche de cette leçon est que le template cesse d’être une « chaîne de chemin » et devienne une dépendance explicite.

Chemins dans les properties et les constantes

Nous avons déjà le moyen de transformer un location en Resource — via @Bean, @Value ou ResourceLoader. La question ici est autre : où garder la source de vérité pour les locations eux‑mêmes.

Il est très facile de tomber dans l’extrême : soit « on code tout en dur », soit « tout dans les properties ». Les deux options peuvent être mauvaises, simplement pour des raisons différentes. Si vous externalisez dans la configuration tous les chemins, vous obtiendrez un fichier de config ressemblant à un annuaire téléphonique : beaucoup de lignes, peu de sens, mais la moindre faute de frappe casse l’application.

La règle pratique est simple : si le chemin vers le template est une partie stable de l’application, il est normal de le garder dans le classpath et de le fixer comme constante. C’est comme une classe Java : vous n’externalisez pas le chemin vers OrderPlacementService dans les properties. Il fait simplement partie du programme.

Pour le ContextFlow actuel, l’option la plus simple et attendue est la suivante : les templates se trouvent dans le classpath et voyagent avec l’application. file: reste une alternative utile quand le template doit vraiment être externalisé.

Mais si vous devez vraiment changer les templates sans recompilation (ou par profil), alors oui — le chemin doit devenir un paramètre. Dans notre cours, c’est particulièrement logique car nous avons déjà une configuration externe et des profils.

Par exemple, dans contextflow.properties, on peut ajouter les clés suivantes :

# Paramètres des chemins vers les templates en tant que chaînes Resource (classpath:... / file:...).
# Idée : on peut changer l'emplacement des templates sans recompilation de l'application.
contextflow.templates.notifications.order-created=classpath:templates/notifications/order-created.txt
contextflow.templates.notifications.order-cancelled=classpath:templates/notifications/order-cancelled.txt
contextflow.templates.reports.daily-header=classpath:templates/reports/daily-report-header.txt

Et alors vous pouvez assembler NotificationTemplates non pas via ClassPathResource, mais via @Value, en laissant le conteneur convertir lui‑même la chaîne location en Resource :

import com.example.contextflow.infrastructure.resources.NotificationTemplates;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;

@Bean
NotificationTemplates notificationTemplates(
        @Value("${contextflow.templates.notifications.order-created}") Resource created,
        @Value("${contextflow.templates.notifications.order-cancelled}") Resource cancelled) {

    // Spring convertira lui-même la chaîne du type classpath:.../file:... en Resource.
    return new NotificationTemplates(created, cancelled);
}

La beauté de cette approche est que vous pouvez plus tard (sans modifier le code Java) remplacer classpath: par file: et fournir un autre fichier. Mais il est important de se rappeler : la flexibilité doit être justifiée. Si vous avez ajouté la config seulement parce que « c’est comme ça dans Spring », vous avez simplement augmenté le nombre d’endroits où l’on peut se tromper.

Schéma des dépendances

Quand le projet grandit, il devient difficile pour le cerveau de garder en tête « qui connaît quoi ». Et avec les ressources, le débutant fait souvent l’une des deux erreurs : soit il traîne ResourceLoader dans le service métier, soit inversement il « cache » les chemins profondément dans le code. Nous avons besoin d’un équilibre : la couche métier connaît le sens (« j’ai besoin du texte de notification »), l’infrastructure connaît la technique (« il se trouve dans le classpath »).

Voici un schéma simple des dépendances (sans détails I/O), qu’il est pratique de garder en tête :

flowchart TD
  A["NotificationDispatchService
(application/service)"] --> B["NotificationTemplateRegistry
(infrastructure/resources)"] B --> C["NotificationTemplates
(infrastructure/resources)"] C --> D["Resource
order-created.txt / order-cancelled.txt"] R["ReportingService
(application/reporting)"] --> H["ReportTemplates
(infrastructure/resources)"] H --> HR["Resource
daily-report-header.txt"]

Le principal « test de bon sens » ici est le suivant : la couche application ne doit pas construire des chaînes de chemins et ne doit pas penser à savoir si c’est classpath: ou file:. Elle doit recevoir des dépendances prêtes qui reflètent le sens. Toute la connaissance de l’emplacement de la ressource est concentrée dans l’infrastructure et la configuration.

6. Erreurs typiques lors du travail avec les ressources

Erreur n°1 : stocker les templates à côté des classes Java dans src/main/java.
Parfois, on met le template dans le même package que le service, « pour que ce soit à côté ». Cela casse rapidement la compréhension de la structure : le code Java se mélange avec les données. De plus, vous perdez la mécanique standard des ressources classpath. Gardez les fichiers d’entrée dans src/main/resources — c’est là que vous, la build, et tous les outils environnants les attendent.

Erreur n°2 : donner aux fichiers des noms sans signification scénaristique.
template1.txt et header2.txt semblent innocents… jusqu’à ce qu’il y en ait 12. Ensuite, vous les ouvrirez un par un en devinant lequel est « le bon ». Des noms comme order-created.txt et daily-report-header.txt vous font gagner du temps, des nerfs et réduisent le nombre de commentaires sarcastiques dans les commits.

Erreur n°3 : dupliquer les chemins vers les ressources en chaînes dans différentes classes.
Quand le même chemin est écrit à 3 endroits, il y aura forcément une faute de frappe ou une « version obsolète » dans l’un d’eux. Pire, vous cessez de faire confiance au code : on ne sait pas où est la source de vérité. Les constantes (ou properties) doivent définir le chemin de manière centralisée.

Erreur n°4 : mélanger les ressources d’entrée et les artefacts de sortie.
Parfois, un développeur écrit un rapport généré dans src/main/resources — « pour qu’il soit à côté du template ». C’est méthodologiquement nocif : les ressources sont l’entrée, build/ est la sortie. Si vous mélangez ces mondes, dans une semaine vous ne saurez plus ce qui fait partie de l’application et ce qui est le résultat d’une exécution précédente.

Erreur n°5 : transformer la configuration en entrepôt de chemins « au cas où ».
Externaliser les chemins dans les properties est une bonne idée seulement si vous prévoyez réellement de les modifier. Si vous avez tout mis dans la config, vous avez gagné plus de flexibilité… et plus d’endroits où vous pouvez vous tromper. Pour les templates stables, une constante classpath est souvent plus simple et plus fiable.

1
Mission
Spring Core, niveau 15, leçon 3
Bloqué
Regroupement de modèles de notification dans un seul bean
Regroupement de modèles de notification dans un seul bean
1
Mission
Spring Core, niveau 15, leçon 3
Bloqué
Beans Resource nommés pour notifications et rapport
Beans Resource nommés pour notifications et rapport
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION