Zum Inhalt springen

Unabhängiges F&E-Projekt · Köln

Java und Spring

Sealed Types, Bean Validation, Micrometer, Spring Modulith und ArchUnit, und die Stellen, an denen eine deklarierte Eigenschaft nicht mehr gilt.

Nicht normativ

Companion-Version
1.0
Bezug zu COADF Core
2.2
Status
Aktuell
Zuletzt geprüft
Profilversion
1.0
Codebeispiele
Codebeispiele folgen in einer späteren Revision.

Codebeispiele folgen in einer späteren Revision.

Behandelte Architektureigenschaften

  • P-1Eine versiegelte Hierarchie, die Vorschläge von verifizierten Werten trennt, Validierung an der Web-Grenze und Modulregeln, die der Build verifiziert.
  • P-4Micrometer Observation und Tracing für den Ausführungskontext, die Audit-Identität in jedem Vertrag und Audit-Einträge, die innerhalb der fachlichen Transaktion geschrieben werden.
  • P-5Herkunft als Felder der Werttypen, explizit serialisiert.
  • P-6Architekturverifikation in der Testphase und Pflicht-Checks, die nicht dadurch bestehen können, dass sie übersprungen werden.
  • P-7Ports im Domänenmodul und Adapter in eigenen Modulen.
  • P-8Eine Policy-Schnittstelle mit einem Engine-Adapter und Entscheidungen, die mit ihrer Revision festgehalten werden.
  • P-2, P-3Nur in der Tiefe, in der COADF sie veröffentlicht.

Architektonische Absicht

Springs Stärke für diese Eigenschaften ist die Deklaration: Constraints an Typen, Transaktionen und Validierung, die der Container anwendet, Observations, die einmal konfiguriert werden, Modulregeln, die ein Test prüft. Sein typisches Versagen ist die Kehrseite desselben Designs. Eine Deklaration gilt auf den Pfaden, die der Container abfängt, über einen Proxy, an einem Web-Binding oder auf einem Executor, den er konfiguriert hat, und nirgendwo sonst. Eine als Annotation geschriebene Eigenschaft ist nur so stark wie die Menge der Aufrufe, die tatsächlich durch Spring laufen.

Dieses Profil bildet jede Eigenschaft auf Spring Boot 4 und Spring Framework 7 ab, die zum Zeitpunkt des Schreibens aktuelle Generation, und benennt die Stellen, an denen eine deklarierte Eigenschaft stillschweigend nicht mehr gilt.

Technologiezuordnung

Java und Spring: Technologiezuordnung
ArchitektureigenschaftJava und Spring
GrenztypEin sealed Interface mit record-Implementierungen: ein Vorschlagstyp und ein verifizierter Typ, in verschiedenen Modulen konstruiert. Records sind flach unveränderliche Datenträger: Eine Komponente, die eine veränderliche Liste ist, bleibt veränderlich, deshalb im Konstruktor kopieren. Sealed Classes sind seit Java 17 ein Standardfeature.
LaufzeitvalidierungJakarta-Validation-Constraints an Request-Typen, ausgelöst mit @Valid. Eine fehlgeschlagene @RequestBody-Validierung löst MethodArgumentNotValidException aus, standardmäßig mit 400 beantwortet.
Unbekannte FelderEine explizite, getestete Entscheidung an Grenztypen über Properties, die der Typ nicht deklariert, statt der jeweils aktuellen Konfiguration des JSON-Mappers.
ArchitekturregelApplicationModules.verify() von Spring Modulith oder ArchUnit-Regeln, als Unit-Tests geschrieben.
AusführungskontextMicrometer Observation und Micrometer Tracing, an OpenTelemetry angebunden.
Asynchroner KontextContextPropagatingTaskDecorator mit der Micrometer-Bibliothek context-propagation; für den automatisch konfigurierten Executor die Boot-Property spring.task.execution.propagate-context.
PrüfspurEinträge, die in der fachlichen Transaktion geschrieben werden; Datenbank-Grants und Trigger wie im PostgreSQL-Beispiel; die Event Publication Registry von Spring Modulith für Events, die nicht verloren gehen dürfen.
Isolierung von StandardsPort-Interfaces im Domänenmodul; ein Adaptermodul pro externem System; der Client des Anbieters für die Domäne nie sichtbar.
PolicyEine Policy-Schnittstelle, die der Domäne gehört, mit einem Adapter für Open Policy Agent, oder Regeltabellen; die Revision an der Entscheidung gespeichert.
IntegrationstestsEin echtes PostgreSQL über Testcontainers, angebunden mit @ServiceConnection.

Referenzmuster

Codebeispiele folgen in einer späteren Revision. Sie werden zurückgestellt, bis eine prüfende Person aus diesem Ökosystem sie gelesen hat, statt geschrieben zu werden, damit die vier Profile gleich aussehen. Die folgende Struktur ist das Referenzmuster, das diese Beispiele umsetzen werden.

Module

  • inference ruft das Modell auf und gibt ein Proposal zurück, ein Record, das den Wert, die Methode und die Herkunft trägt. Es hat keine Abhängigkeit von records oder audit, und eine Modulith-Verifikation oder eine ArchUnit-Regel hält das in einem Test fest.
  • records ist das einzige Modul, das einen VerifiedValue erzeugen kann. Der Typ ist außerhalb des Moduls nicht öffentlich, oder er ist eine Klasse, deren Konstruktor es nicht ist: Ein öffentliches Record kann seinen Konstruktor nicht verbergen, denn der kanonische Konstruktor eines öffentlichen Records muss selbst öffentlich sein. Die Erzeugung läuft über eine Factory, die die Verifikation durch eine namentlich benannte Person verlangt.
  • audit hängt Einträge mit den veröffentlichten Feldern an, innerhalb der Transaktion der Änderung, die sie festhalten, über ein Repository, das weder Update noch Delete anbietet.
  • classification deklariert den Port; classification.vendor implementiert ihn und ist das einzige Modul, das den Client des Anbieters oder dessen Typen sehen kann.
  • policy deklariert die Policy-Schnittstelle und den Entscheidungsdatensatz; ein Adaptermodul spricht mit der Engine.

Grenzen zur Laufzeit

Validiert wird am Web-Binding, wo Spring Constraints am Request-Body auswertet, und noch einmal in der Domänen-Factory, denn das Web-Binding ist ein Einstiegspunkt unter mehreren: Ein Message-Listener, ein Batch-Job und ein Test erreichen die Domäne alle, ohne es zu passieren.

Observations umschließen die Grenze und die externen Aufrufe. Jeder Executor, der Arbeit für eine Anfrage ausführt, ist so konfiguriert, dass er den Kontext weitergibt, und die Audit-Identität ist ein Feld jeder Nachricht und jedes Job-Arguments, nie nachträglich aus einem Thread-Local zurückgewonnen.

Fehlermuster

  1. Selbstaufruf umgeht den Proxy

    Spring AOP basiert auf Proxies: Ein Aufruf von einer Methode einer Bean an eine andere Methode derselben Bean läuft nicht durch den Proxy, deshalb gelten Methodenvalidierung und @Transactional für ihn nicht. Ein Constraint, der für Aufrufer außerhalb der Bean gilt, gilt nicht für die Bean selbst, und dort beginnt oft der Batch-Pfad.

  2. Constraints deklariert, nie ausgewertet

    Jakarta-Validation-Constraints an einem Typ bewirken für sich allein nichts. Ausgewertet werden sie dort, wo etwas die Validierung auslöst: @Valid oder @Validated an einem Web-Binding oder Methodenvalidierung über den Proxy. Ein Message-Listener, der denselben Typ empfängt, validiert nichts, solange er es nicht anfordert.

  3. Nicht deklarierte Properties der Mapper-Konfiguration überlassen

    Ob ein eingehendes JSON-Dokument Properties tragen darf, die der Typ nicht deklariert, ist eine Einstellung des JSON-Mappers. Für einen Grenztyp ist die richtige Antwort meist, sie abzulehnen, und diese Entscheidung gehört an den Typ und in einen Test, nicht in eine Anwendungs-Property, die jemand anderes ändern kann.

  4. ORM-Unveränderlichkeit für eine Audit-Absicherung gehalten

    Hibernate ignoriert Änderungen im Speicher an einer verwalteten @Immutable-Entität, ohne Update und ohne Exception. Bulk-Updates dagegen werfen in Hibernate 7 standardmäßig eine Exception und erzeugten in 6.6 nur eine Warnung. Nichts davon hält einen zweiten Client, ein Skript oder eine Migration davon ab, die Tabelle zu ändern; der Schutz gehört in die Datenbank.

  5. Audit-Events nach dem Commit verloren

    Ein Event, das nach dem Commit der Transaktion behandelt wird, geht verloren, wenn der Handler fehlschlägt, es sei denn, etwas hat es festgehalten. Die Event Publication Registry von Spring Modulith schreibt einen Eintrag in der veröffentlichenden Transaktion und bewahrt fehlgeschlagene Veröffentlichungen für eine erneute Übermittlung auf; sie beim Neustart automatisch erneut zu veröffentlichen, ist eine Einstellung, die eigens eingeschaltet werden muss.

  6. Ein öffentliches Record als verifizierter Typ

    Der kanonische Konstruktor eines öffentlichen Records muss öffentlich sein, also kann jeder Code, der den Typ sieht, einen verifizierten Wert ohne Verifikation bauen. Den Typ außer Reichweite halten oder eine Klasse mit privatem Konstruktor und einer Factory verwenden.

  7. Zwei Tracing-Bridges

    Micrometer Tracing bindet an Brave oder an OpenTelemetry an, und seine Dokumentation verlangt, nur eines davon zu wählen. Zwei auf dem Classpath erzeugen Traces, die davon abhängen, welche sich durchsetzt.

  8. Integrationstests gegen eine andere Datenbank

    Grants, Trigger und das Verhalten von Constraints sind Eigenschaften der echten Datenbank. Tests gegen einen eingebetteten Ersatz verifizieren den Code und nicht die Eigenschaft.

Verifikation

  • Unit-Test

    Bestanden, wenn: Der verifizierte Typ lässt sich nur über die Factory erzeugen, und nur mit einer Verifikation durch eine namentlich benannte Person; eine Ablehnung lässt das Attribut leer.

    Zahnbeweis: Den verifizierten Typ in einem Wegwerf-Branch öffentlich machen: Ein Test, der prüft, dass er aus dem Inferenzmodul nicht zugänglich ist, schlägt fehl.

  • Architekturtest

    Bestanden, wenn: Die Verifikation von Spring Modulith besteht, oder die ArchUnit-Regeln bestehen: keine Abhängigkeit von inference zu records oder audit, kein Anbietertyp in der Domäne. ArchUnit prüft Abhängigkeiten zwischen Paketen und Klassen als gewöhnliche Unit-Tests, und die Verifikation von Modulith lehnt Zyklen zwischen Modulen und Verweise in die internen Pakete eines anderen Moduls ab, außer in Module, die als offen deklariert sind. Beide Werkzeuge prüfen die aufgeschriebenen Regeln; ob es die richtigen Regeln sind, ist Designsache des Teams.

    Zahnbeweis: Die verbotene Abhängigkeit einbauen: Die Verifikation schlägt fehl.

  • Integrationstest

    Bestanden, wenn: Gegen PostgreSQL aus Testcontainers: Die Rolle der Anwendung kann die Prüfspur weder ändern noch löschen, und Einträge werden in derselben Transaktion wie die Änderung geschrieben.

    Zahnbeweis: Die fachliche Transaktion nach dem Schreiben zurückrollen: Der Audit-Eintrag darf das nicht überstehen.

  • Integrationstest

    Bestanden, wenn: Der Kontext übersteht die Executors, die die Anwendung verwendet: Ein In-Memory-Exporter zeigt die asynchrone Arbeit als Teil desselben Traces, und die Audit-Identität steht in ihrem Datensatz.

    Zahnbeweis: Den Task-Decorator entfernen: Der Test schlägt fehl.

  • Vertragstest

    Bestanden, wenn: Produzent und Konsument stimmen bei den Verträgen für Vorschlag und Entscheidung überein, mit der Herkunft und der Revision als Pflichtfeldern.

  • End-to-End-Test

    Bestanden, wenn: Eine Ausgabe, deren Attribut nicht verifiziert wurde, kann nicht veröffentlicht werden, gleich über welchen Einstiegspunkt die Anfrage kam.

Alternative Umsetzungen

  • Andere Java-Frameworks (Quarkus, Micronaut, Jakarta EE) drücken dieselbe Grenze mit anderen Interception-Modellen aus; die Frage nach dem Selbstaufruf muss an jedes gestellt werden.
  • Explizites SQL (jOOQ oder reines JDBC) für die Prüfspur statt eines ORM, wo die genauen Statements wichtiger sind als der Komfort des Mappings.
  • Event-Sourcing-Frameworks, wo die Historie das Modell ist.
  • Regeltabellen, die der Anwendung gehören, statt einer externen Policy-Engine, mit derselben Anforderung an die Revision.

Abwägungen

  • Deklarativ ist knapp und unsichtbar. An der Aufrufstelle ist nicht zu erkennen, ob ein Constraint oder eine Transaktion gilt; das müssen Tests zeigen.
  • Spring Modulith kodiert Konventionen, was schnell einzuführen und meinungsstark ist; ArchUnit ist allgemein und verlangt, die Regeln auszuschreiben.
  • JPA ist bequem und legt eine Schicht zwischen den Code und die eigenen Schutzmechanismen der Datenbank. Für die Prüfspur ist diese Schicht genau das, was durchschaut werden muss.
  • Modulgrenzen innerhalb einer deploybaren Einheit halten das Betriebsmodell einfach und machen die Grenze vollständig von der Verifikation im Build abhängig.

Grenzen

  • Für dieses Profil wird in Companion 1.0 kein Code veröffentlicht, und kein Beispiel wurde ausgeführt. Das beschriebene Verhalten stammt aus der aktuellen Dokumentation der Hersteller, eingesehen am 11. September 2026, für Spring Boot 4.1, Spring Framework 7.0, Spring Modulith 2.1, Hibernate ORM 7.4 und Java SE 25.
  • Nichts hier zeigt, wie Konfidenz dargestellt wird, wann eine Prüfung erforderlich ist oder wie die Prüfung organisiert ist; diese Teile von P-2 und P-3 veröffentlicht COADF nicht.
  • Alles Zitierte ist Verhalten der Sprache oder des Frameworks. Die Architektureigenschaft, dass ein Vorschlag ohne Verifikation nicht zu einem Datensatz werden kann, ist ein Design, das das Team noch entwerfen und testen muss: Keine Annotation, kein Record und keine Modulregel stellt sie von sich aus her.

Was dieses Profil nicht begründet

Wer diesem Profil folgt, begründet damit weder die Einhaltung regulatorischer Anforderungen noch eine Zertifizierung oder ein Verfahren zur Bewertung der Übereinstimmung mit Rechtsvorschriften, und Spring wird von COADF nicht vorausgesetzt.

Getestete Referenzumgebung

Für dieses Profil wurde kein Beispiel ausgeführt.

Quellen

COADF Engineering Companion 1.0 · nicht normativ · bezieht sich auf COADF Core 2.2

Veröffentlichungsrechte vorbehalten. Für den COADF Engineering Companion 1.0 und seine Referenzbeispiele wird derzeit keine öffentliche Lizenz erteilt.

Schutzrechts- und Veröffentlichungsstatus