Aller au contenu

Projet indépendant de R&D · Cologne

Java et Spring

Types scellés, Bean Validation, Micrometer, Spring Modulith et ArchUnit, et les endroits où une propriété déclarée cesse de s'appliquer.

Non normatif

Version du Companion
1.0
Se rapporte à COADF Core
2.2
Statut
À jour
Dernière relecture
Version du profil
1.0
Exemples de code
Les exemples de code suivront dans une révision ultérieure.

Les exemples de code suivront dans une révision ultérieure.

Propriétés d'architecture traitées

  • P-1Une hiérarchie scellée qui sépare les propositions des valeurs vérifiées, une validation à la frontière web, et des règles de modules que le build vérifie.
  • P-4Micrometer Observation et Tracing pour le contexte d'exécution, l'identité d'audit dans chaque contrat, et des entrées d'audit écrites à l'intérieur de la transaction métier.
  • P-5La provenance comme champs des types de valeur, sérialisée explicitement.
  • P-6Une vérification de l'architecture dans la phase de test, et des checks requis qui ne peuvent pas passer en étant ignorés.
  • P-7Des ports dans le module du domaine et des adaptateurs dans des modules à part.
  • P-8Une interface de politique avec un adaptateur de moteur, et des décisions enregistrées avec leur révision.
  • P-2, P-3Seulement à la profondeur que COADF publie.

Intention d'architecture

La force de Spring pour ces propriétés est la déclaration : des contraintes sur les types, des transactions et une validation appliquées par le conteneur, des observations configurées une fois, des règles de modules contrôlées par un test. Sa défaillance caractéristique est l'autre face de la même conception. Une déclaration tient sur les chemins que le conteneur intercepte, à travers un proxy, sur une liaison web ou sur un exécuteur qu'il a configuré, et nulle part ailleurs. Une propriété écrite sous forme d'annotation ne vaut que par l'ensemble des appels qui passent réellement par Spring.

Ce profil rapporte chaque propriété à Spring Boot 4 et Spring Framework 7, la génération actuelle au moment de sa rédaction, et nomme les endroits où une propriété déclarée cesse discrètement de s'appliquer.

Correspondance technologique

Java et Spring: Correspondance technologique
Propriété d'architectureJava et Spring
Type de frontièreUne interface sealed avec des implémentations record : un type de proposition et un type vérifié, construits dans des modules différents. Les records sont des porteurs de données superficiellement immuables : un composant qui est une liste mutable reste mutable, il faut donc le copier dans le constructeur. Les classes scellées sont une fonctionnalité standard depuis Java 17.
Validation à l'exécutionDes contraintes Jakarta Validation sur les types de requête, déclenchées avec @Valid. Une validation @RequestBody en échec lève MethodArgumentNotValidException, qui reçoit par défaut une réponse 400.
Champs inconnusUne décision explicite et testée, sur les types de frontière, au sujet des propriétés que le type ne déclare pas, plutôt que ce que la configuration du mapper JSON se trouve être à ce moment-là.
Règle d'architectureApplicationModules.verify() de Spring Modulith, ou des règles ArchUnit écrites comme tests unitaires.
Contexte d'exécutionMicrometer Observation et Micrometer Tracing, reliés à OpenTelemetry.
Contexte asynchroneContextPropagatingTaskDecorator avec la bibliothèque context-propagation de Micrometer ; pour l'exécuteur auto-configuré, la propriété Boot spring.task.execution.propagate-context.
Piste d'auditDes entrées écrites dans la transaction métier ; des droits et des triggers de base de données comme dans l'exemple PostgreSQL ; le registre de publication d'événements de Spring Modulith pour les événements qui ne doivent pas être perdus.
Isolation des normesDes interfaces de port dans le module du domaine ; un module d'adaptateur par système externe ; le client du fournisseur jamais visible du domaine.
PolitiqueUne interface de politique appartenant au domaine avec un adaptateur Open Policy Agent, ou des tables de règles ; la révision stockée sur la décision.
Tests d'intégrationUn vrai PostgreSQL via Testcontainers, connecté avec @ServiceConnection.

Pattern de référence

Les exemples de code suivront dans une révision ultérieure. Ils sont reportés jusqu'à ce qu'une personne qui vérifie et travaille dans cet écosystème les ait lus, plutôt qu'écrits pour que les quatre profils se ressemblent. La structure ci-dessous est le pattern de référence que ces exemples mettront en œuvre.

Modules

  • inference appelle le modèle et renvoie un Proposal, un record qui porte la valeur, la méthode et la provenance. Il ne dépend ni de records ni de audit, et une vérification Modulith ou une règle ArchUnit l'affirme dans un test.
  • records est le seul module qui peut créer une VerifiedValue. Le type n'est pas public hors du module, ou bien c'est une classe dont le constructeur ne l'est pas : un record public ne peut pas cacher son constructeur, car le constructeur canonique d'un record public doit lui-même être public. La création passe par une fabrique qui exige la vérification d'une personne nommée.
  • audit ajoute des entrées avec les champs publiés, dans la transaction de la modification qu'il enregistre, via un repository qui n'offre ni mise à jour ni suppression.
  • classification déclare le port ; classification.vendor le met en œuvre, et c'est le seul module qui peut voir le client du fournisseur ou ses types.
  • policy déclare l'interface de politique et l'enregistrement de décision ; un module d'adaptateur dialogue avec le moteur.

Les frontières à l'exécution

La validation a lieu sur la liaison web, où Spring évalue les contraintes du corps de la requête, puis de nouveau dans la fabrique du domaine, car la liaison web n'est qu'un point d'entrée parmi d'autres : un listener de messages, un job batch et un test atteignent tous le domaine sans passer par elle.

Les observations enveloppent la frontière et les appels externes. Chaque exécuteur qui exécute du travail pour une requête est configuré pour propager le contexte, et l'identité d'audit est un champ de chaque message et de chaque argument de job, jamais récupérée après coup depuis un thread-local.

Modes de défaillance

  1. L'auto-invocation contourne le proxy

    Spring AOP repose sur des proxys : l'appel d'une méthode d'un bean à une autre méthode du même bean ne passe pas par le proxy, si bien que la validation de méthode et @Transactional ne s'y appliquent pas. Une contrainte qui tient pour les appelants extérieurs au bean ne tient pas pour le bean lui-même, et c'est souvent là que commence le chemin batch.

  2. Des contraintes déclarées, jamais évaluées

    Les contraintes Jakarta Validation sur un type ne font rien par elles-mêmes. Elles sont évaluées là où quelque chose déclenche la validation : @Valid ou @Validated sur une liaison web, ou la validation de méthode via le proxy. Un listener de messages qui reçoit le même type ne valide rien s'il ne le demande pas.

  3. Des propriétés non déclarées laissées à la configuration du mapper

    Qu'un document JSON entrant puisse porter des propriétés que le type ne déclare pas est un réglage du mapper JSON. Pour un type de frontière, la bonne réponse est généralement de les refuser, et cette décision a sa place sur le type et dans un test, pas dans une propriété d'application que quelqu'un d'autre peut modifier.

  4. L'immuabilité de l'ORM prise pour une protection de l'audit

    Hibernate ignore les modifications en mémoire d'une entité @Immutable gérée, sans mise à jour et sans exception. Les mises à jour en masse sur une telle entité lèvent une exception par défaut dans Hibernate 7 et ne produisaient qu'un avertissement en 6.6. Rien de cela n'empêche un second client, un script ou une migration de mettre à jour la table ; la protection a sa place dans la base de données.

  5. Des événements d'audit perdus après le commit

    Un événement traité après le commit de la transaction est perdu si le handler échoue, à moins que quelque chose ne l'ait enregistré. Le registre de publication d'événements de Spring Modulith écrit une entrée dans la transaction de publication et conserve les publications en échec pour les soumettre à nouveau ; les republier automatiquement au redémarrage est un réglage qu'il faut activer.

  6. Un record public comme type vérifié

    Le constructeur canonique d'un record public doit être public : tout code qui voit le type peut donc construire une valeur vérifiée sans vérification. Garder le type hors de portée, ou utiliser une classe avec un constructeur privé et une fabrique.

  7. Deux ponts de tracing

    Micrometer Tracing fait le pont vers Brave ou vers OpenTelemetry, et sa documentation demande de n'en choisir qu'un. Deux sur le classpath produisent des traces qui dépendent de celui qui l'emporte.

  8. Des tests d'intégration contre une autre base de données

    Les droits, les triggers et le comportement des contraintes sont des propriétés de la vraie base de données. Des tests contre un substitut embarqué contrôlent le code et non la propriété.

Vérification

  • Test unitaire

    Réussit quand : Le type vérifié ne peut être créé que par la fabrique, et seulement avec une vérification par une personne nommée ; un rejet laisse l'attribut vide.

    Preuve de dents : Rendre le type vérifié public dans une branche jetable : un test qui affirme qu'il n'est pas accessible depuis le module d'inférence échoue.

  • Test d'architecture

    Réussit quand : La vérification de Spring Modulith passe, ou les règles ArchUnit passent : aucune dépendance de inference vers records ou audit, aucun type de fournisseur dans le domaine. ArchUnit contrôle les dépendances entre paquets et classes sous forme de simples tests unitaires, et la vérification de Modulith rejette les cycles entre modules et les références vers les paquets internes d'un autre module, sauf vers les modules déclarés ouverts. Chaque outil contrôle les règles écrites ; que ce soient les bonnes règles relève de la conception de l'équipe.

    Preuve de dents : Planter la dépendance interdite : la vérification échoue.

  • Test d'intégration

    Réussit quand : Contre PostgreSQL lancé par Testcontainers : le rôle de l'application ne peut ni mettre à jour ni supprimer la piste, et les entrées sont écrites dans la même transaction que la modification.

    Preuve de dents : Annuler la transaction métier après l'écriture : l'entrée d'audit ne doit pas y survivre.

  • Test d'intégration

    Réussit quand : Le contexte survit aux exécuteurs qu'utilise l'application : un exportateur en mémoire montre le travail asynchrone comme partie de la même trace, et l'identité d'audit est présente dans son enregistrement.

    Preuve de dents : Retirer le décorateur de tâches : le test échoue.

  • Test de contrat

    Réussit quand : Le producteur et le consommateur s'accordent sur les contrats de proposition et de décision, avec la provenance et la révision comme champs obligatoires.

  • Test de bout en bout

    Réussit quand : Une sortie dont l'attribut n'a pas été vérifié ne peut pas être publiée, quel que soit le point d'entrée par lequel la requête est arrivée.

Réalisations alternatives

  • D'autres frameworks Java (Quarkus, Micronaut, Jakarta EE) expriment la même frontière avec d'autres modèles d'interception ; la question de l'auto-invocation doit être posée pour chacun.
  • Du SQL explicite (jOOQ ou JDBC simple) pour la piste d'audit au lieu d'un ORM, là où les instructions exactes comptent davantage que le confort du mapping.
  • Des frameworks d'event sourcing là où l'historique est le modèle.
  • Des tables de règles appartenant à l'application au lieu d'un moteur de politiques externe, avec la même exigence de révision.

Compromis

  • Le déclaratif est concis et invisible. Un lecteur ne peut pas voir, depuis le site d'appel, si une contrainte ou une transaction s'applique ; ce sont les tests qui doivent le montrer.
  • Spring Modulith encode des conventions, ce qui est rapide à adopter et prescriptif ; ArchUnit est générique et demande d'écrire les règles en entier.
  • JPA est pratique et place une couche entre le code et les protections propres à la base de données. Pour la piste d'audit, cette couche est la partie à travers laquelle il faut voir.
  • Des frontières de modules à l'intérieur d'une seule unité déployable gardent le modèle d'exploitation simple, et font dépendre la frontière entièrement de la vérification du build.

Limites

  • Aucun code n'est publié pour ce profil dans Companion 1.0, et aucun exemple n'a été exécuté. Le comportement décrit est tiré de la documentation officielle actuelle, consultée le 11 septembre 2026, pour Spring Boot 4.1, Spring Framework 7.0, Spring Modulith 2.1, Hibernate ORM 7.4 et Java SE 25.
  • Rien ici ne montre comment la confiance est représentée, quand une revue est requise, ni comment la revue est organisée ; COADF ne publie pas ces parties de P-2 et P-3.
  • Tout ce qui est cité relève du comportement du langage ou du framework. La propriété d'architecture, à savoir qu'une proposition ne peut pas devenir un enregistrement sans vérification, est une conception que l'équipe doit encore réaliser et tester : aucune annotation, aucun record ni aucune règle de module ne la fournit à lui seul.

Ce que ce profil n'établit pas

Suivre ce profil n'établit ni la conformité réglementaire, ni une certification, ni une évaluation de la conformité, et COADF n'exige pas Spring.

Environnement de référence testé

Aucun exemple n'a été exécuté pour ce profil.

Sources

COADF Engineering Companion 1.0 · non normatif · se rapporte à COADF Core 2.2

Droits de publication réservés. Aucune licence publique n'est accordée à ce jour pour le COADF Engineering Companion 1.0 ni pour ses exemples de référence.

Statut de propriété intellectuelle et de publication