Modul 03 · Migration Deep Dive · Stufe 3 Application Layer

REST

JAX-RS 2.1 mit javax.ws.rs wird nachvollziehbar in Spring Boot REST mit jakarta.validation überführt. Die Akte zeigt nicht nur das Ziel, sondern die tatsächlichen Dateien, APIs, Dependencies, Codebelege, Tests, Risiken und Cutover-Schritte.

RepositoryService LayerDTO
4 → 5Produktionsdateien
110 → 98Java-Zeilen
1 → 1Testdateien
4 / 4Dependencies entfernt / neu
NIEDRIGRisiko · Score 0

Was sich konkret ändert

DimensionLegacyModernMigrationskonsequenz
ProgrammiermodellConsumes, GET, POST, Path, ProducesGetMapping, NotBlank, PostMapping, Repository, RequestBody, RequestMapping, RestController, Service, SpringBootApplication, ValidAnnotationen und Containerfunktionen werden nur dort eingesetzt, wo sie eine konkrete technische Verantwortung übernehmen.
Abhängigkeiten4 direkte Dependencies4 direkte Dependencies4 neu, 4 entfernt; Versionen und transitive Auswirkungen im erfolgreichen Online-Build prüfen.
Öffentliche API10 erkannte Methoden7 erkannte MethodenMethoden werden nach fachlicher Rolle gemappt; reine Bootstrap- und Framework-Methoden sind kein fachlicher Vertrag.
Datenmodellclass Book, class BookRepository, class BookResource, class RestLegacyApplicationclass BookController, class BookRepository, class BookService, class RestModernApplication, record BookFeldnamen, IDs, Null-Semantik, Gleichheit und Serialisierungsform werden separat regressionstestet.
FehlerverhaltenLegacy-Exceptions und Rückgabewerteexplizitere Fach-/Framework-FehlerabbildungFehler dürfen nicht nur technisch übersetzt werden; Status, Ursache, Retrybarkeit und Client-Vertrag müssen erhalten oder versioniert werden.
TestsBestands- und Golden-Master-TestsUnit-, Slice-, Contract- und IntegrationstestsDer Modern-Pfad wird zuerst gegen denselben fachlichen Vektor geprüft und danach um neue technische Risiken ergänzt.
BetriebLegacy-Start/Lifecyclemodernes Packaging, Health und externe KonfigurationParallelbetrieb, Replay/Retry und Rückrouting müssen vor Abschaltung des Legacy-Pfads erprobt sein.
RollbackLegacy-Artefakt bleibt unverändertModern-Artefakt getrennt deploybarKein Rollback über Datenverlust: Schema, Nachrichten und verschlüsselte Daten müssen rückwärtslesbar oder durch Dual-Read abgesichert sein.

Fachlicher Vertrag: unverändert zu erhalten

  • HTTP-Pfade, Verben, Statuscodes und JSON-Felder werden bewusst versioniert.
  • Book-ID und Repository-Semantik bleiben stabil.
  • Ungültige Eingaben liefern ein dokumentiertes, maschinenlesbares Fehlerformat.

Hauptrisiko und Testfokus

HTTP contract drift and validation gaps

status codes, validation, error payload and actuator exposure

Die Modernisierung gilt erst als abgeschlossen, wenn der fachliche Vertrag automatisiert belegt ist.

Verbindliches Code- und Rollenmapping

Die Zuordnung ist semantisch: Eine Legacy-Klasse kann in mehrere moderne Rollen zerlegt werden.

Legacy-Rolle / DateiModern-Rolle / DateiBedeutung
BookResource.javaBookController.java + BookService.javaResource-Verantwortung wird in HTTP-Adapter und Service Layer getrennt.
BookRepository.javaBookRepository.javaDatenzugriffsvertrag bleibt erhalten.
Book.javaBook.javaAPI-/Domain-Datenform bleibt vergleichbar.
RestLegacyApplication.javaRestModernApplication.javaJersey/Grizzly-Bootstrap wird Spring Boot Bootstrap.

Legacy-Quellinventar

DateiTypAnnotationenöffentliche APIPatternZeilen
Book.javaclass Booklong getId()
void setId(long id)
String getTitle()
void setTitle(String title)
29
BookRepository.javaclass BookRepositoryBookRepository instance()
List<Book> findAll()
Book save(Book book)
Repository32
BookResource.javaclass BookResourceConsumes, GET, POST, Path, ProducesList<Book> all()
Response create(Book book)
28
RestLegacyApplication.javaclass RestLegacyApplicationvoid main(String[] args)21

Modern-Quellinventar

DateiTypAnnotationenöffentliche APIPatternZeilen
Book.javarecord BookNotBlank6
BookController.javaclass BookControllerGetMapping, PostMapping, RequestBody, RequestMapping, RestController, ValidList<Book> all()
ResponseEntity<Book> create(@Valid @RequestBody Book book, UriComponentsBuilder uriBuilder)
31
BookRepository.javaclass BookRepositoryRepositoryList<Book> findAll()
Book save(Book input)
Repository28
BookService.javaclass BookServiceServiceList<Book> findAll()
Book create(Book book)
Service Layer22
RestModernApplication.javaclass RestModernApplicationSpringBootApplicationvoid main(String[] args)11

Dependency- und Laufzeitmigration

StatusDependencyVersionScopePrüfung
ENTFERNTorg.glassfish.jersey.containers:jersey-container-grizzly2-http${jersey.version}compileLegacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt.
ENTFERNTorg.glassfish.jersey.inject:jersey-hk2${jersey.version}compileLegacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt.
ENTFERNTorg.glassfish.jersey.media:jersey-media-json-jackson${jersey.version}compileLegacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt.
ENTFERNTorg.junit.jupiter:junit-jupiterBOM/ParenttestLegacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt.
NEUorg.springframework.boot:spring-boot-starter-actuatorBOM/ParentcompileNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
NEUorg.springframework.boot:spring-boot-starter-testBOM/ParenttestNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
NEUorg.springframework.boot:spring-boot-starter-validationBOM/ParentcompileNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
NEUorg.springframework.boot:spring-boot-starter-webBOM/ParentcompileNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
Direkte POM-Daten ersetzen keinen erfolgreichen Online-Build mit transitiver SBOM- und CVE-Prüfung.

Vorher-/Nachher-Codebelege

Die folgenden Ausschnitte stammen direkt aus den enthaltenen Projekten. Dadurch ist sichtbar, welche Verantwortung tatsächlich verschoben wurde.

Legacy
BookResource.java
Modern
BookController.java + BookService.java

Migrationsbedeutung: Resource-Verantwortung wird in HTTP-Adapter und Service Layer getrennt.

Legacy-Code

projects/03-rest/legacy/src/main/java/at/aydin/lab/rest/legacy/BookResource.java

package at.aydin.lab.rest.legacy;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;

@Path("books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
    private final BookRepository repository = BookRepository.instance();
    @GET
    public List<Book> all() {
        return repository.findAll();
    }

    @POST
    public Response create(Book book) {
        Book created = repository.save(book);
        return Response.created(URI.create("/api/books/" + created.getId())).entity(created).build();
    }
}

Modern-Code

projects/03-rest/modern/src/main/java/at/aydin/lab/rest/modern/BookController.java

package at.aydin.lab.rest.modern;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {
    private final BookService service;
    public BookController(BookService service) {
        this.service = service;
    }

    @GetMapping
    public List<Book> all() {
        return service.findAll();
    }

    @PostMapping
    public ResponseEntity<Book> create(@Valid @RequestBody Book book, UriComponentsBuilder uriBuilder) {
        Book created = service.create(book);
        return ResponseEntity.created(uriBuilder.path("/api/books/{id}").build(created.id())).body(created);
    }
}

Modern-Code

projects/03-rest/modern/src/main/java/at/aydin/lab/rest/modern/BookService.java

package at.aydin.lab.rest.modern;

import org.springframework.stereotype.Service;
import java.util.List;

@Service
// Design Pattern: Service Layer
// Zweck: Fachlogik bleibt unabhängig von HTTP.
public class BookService {
    private final BookRepository repository;
    public BookService(BookRepository repository) {
        this.repository = repository;
    }

    public List<Book> findAll() {
        return repository.findAll();
    }

    public Book create(Book book) {
        return repository.save(book);
    }
}
Legacy
BookRepository.java
Modern
BookRepository.java

Migrationsbedeutung: Datenzugriffsvertrag bleibt erhalten.

Legacy-Code

projects/03-rest/legacy/src/main/java/at/aydin/lab/rest/legacy/BookRepository.java

package at.aydin.lab.rest.legacy;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

// Design Pattern: Repository
// Zweck: Datenhaltung wird von der REST-Ressource getrennt.
public final class BookRepository {
    private static final BookRepository INSTANCE = new BookRepository();
    private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong sequence = new AtomicLong();
    private BookRepository() {
        save(new Book(0, "Enterprise Java verstehen"));
    }

    public static BookRepository instance() {
        return INSTANCE;
    }

    public List<Book> findAll() {
        return new ArrayList<>(books.values());
    }

    public Book save(Book book) {
        long id = book.getId() == 0 ? sequence.incrementAndGet() : book.getId();
        Book stored = new Book(id, book.getTitle());
        books.put(id, stored);
        return stored;
    }
}

Modern-Code

projects/03-rest/modern/src/main/java/at/aydin/lab/rest/modern/BookRepository.java

package at.aydin.lab.rest.modern;

import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Repository
// Design Pattern: Repository
// Zweck: Der Controller kennt keine Details der Datenhaltung.
public class BookRepository {
    private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong sequence = new AtomicLong();
    public BookRepository() {
        save(new Book(0, "Modernes Enterprise Java"));
    }

    public List<Book> findAll() {
        return List.copyOf(books.values());
    }

    public Book save(Book input) {
        long id = input.id() == 0 ? sequence.incrementAndGet() : input.id();
        Book stored = new Book(id, input.title());
        books.put(id, stored);
        return stored;
    }
}
Technischen Unified Diff öffnen
--- BookRepository.java
+++ BookRepository.java
@@ -1,31 +1,27 @@
-package at.aydin.lab.rest.legacy;
+package at.aydin.lab.rest.modern;
 
-import java.util.ArrayList;
+import org.springframework.stereotype.Repository;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
 
+@Repository
 // Design Pattern: Repository
-// Zweck: Datenhaltung wird von der REST-Ressource getrennt.
-public final class BookRepository {
-    private static final BookRepository INSTANCE = new BookRepository();
+// Zweck: Der Controller kennt keine Details der Datenhaltung.
+public class BookRepository {
     private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
     private final AtomicLong sequence = new AtomicLong();
-    private BookRepository() {
-        save(new Book(0, "Enterprise Java verstehen"));
-    }
-
-    public static BookRepository instance() {
-        return INSTANCE;
+    public BookRepository() {
+        save(new Book(0, "Modernes Enterprise Java"));
     }
 
     public List<Book> findAll() {
-        return new ArrayList<>(books.values());
+        return List.copyOf(books.values());
     }
 
-    public Book save(Book book) {
-        long id = book.getId() == 0 ? sequence.incrementAndGet() : book.getId();
-        Book stored = new Book(id, book.getTitle());
+    public Book save(Book input) {
+        long id = input.id() == 0 ? sequence.incrementAndGet() : input.id();
+        Book stored = new Book(id, input.title());
         books.put(id, stored);
         return stored;
     }
Legacy
Book.java
Modern
Book.java

Migrationsbedeutung: API-/Domain-Datenform bleibt vergleichbar.

Legacy-Code

projects/03-rest/legacy/src/main/java/at/aydin/lab/rest/legacy/Book.java

package at.aydin.lab.rest.legacy;

public class Book {
    private long id;
    private String title;
    public Book() {
    }

    public Book(long id, String title) {
        this.id = id;
        this.title = title;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

Modern-Code

projects/03-rest/modern/src/main/java/at/aydin/lab/rest/modern/Book.java

package at.aydin.lab.rest.modern;

import jakarta.validation.constraints.NotBlank;

public record Book(long id, @NotBlank String title) {
}
Technischen Unified Diff öffnen
--- Book.java
+++ Book.java
@@ -1,29 +1,6 @@
-package at.aydin.lab.rest.legacy;
+package at.aydin.lab.rest.modern;
 
-public class Book {
-    private long id;
-    private String title;
-    public Book() {
-    }
+import jakarta.validation.constraints.NotBlank;
 
-    public Book(long id, String title) {
-        this.id = id;
-        this.title = title;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
+public record Book(long id, @NotBlank String title) {
 }
Legacy
RestLegacyApplication.java
Modern
RestModernApplication.java

Migrationsbedeutung: Jersey/Grizzly-Bootstrap wird Spring Boot Bootstrap.

Legacy-Code

projects/03-rest/legacy/src/main/java/at/aydin/lab/rest/legacy/RestLegacyApplication.java

package at.aydin.lab.rest.legacy;

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import java.net.URI;

public final class RestLegacyApplication {
    private RestLegacyApplication() {
    }

    public static void main(String[] args) throws Exception {
        ResourceConfig config = new ResourceConfig(BookResource.class, JacksonFeature.class);
        HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create("http://localhost:8081/api/"), config);
        System.out.println("Legacy JAX-RS: http://localhost:8081/api/books");
        System.out.println("ENTER beendet den Server.");
        System.in.read();
        server.shutdownNow();
    }
}

Modern-Code

projects/03-rest/modern/src/main/java/at/aydin/lab/rest/modern/RestModernApplication.java

package at.aydin.lab.rest.modern;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RestModernApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestModernApplication.class, args);
    }
}
Technischen Unified Diff öffnen
--- RestLegacyApplication.java
+++ RestModernApplication.java
@@ -1,21 +1,11 @@
-package at.aydin.lab.rest.legacy;
+package at.aydin.lab.rest.modern;
 
-import org.glassfish.grizzly.http.server.HttpServer;
-import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
-import org.glassfish.jersey.jackson.JacksonFeature;
-import org.glassfish.jersey.server.ResourceConfig;
-import java.net.URI;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
 
-public final class RestLegacyApplication {
-    private RestLegacyApplication() {
-    }
-
-    public static void main(String[] args) throws Exception {
-        ResourceConfig config = new ResourceConfig(BookResource.class, JacksonFeature.class);
-        HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create("http://localhost:8081/api/"), config);
-        System.out.println("Legacy JAX-RS: http://localhost:8081/api/books");
-        System.out.println("ENTER beendet den Server.");
-        System.in.read();
-        server.shutdownNow();
+@SpringBootApplication
+public class RestModernApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(RestModernApplication.class, args);
     }
 }

Umsetzungsplan mit Qualitäts-Gates

  1. Arbeitspaket 1
    JAX-RS-Vertrag aus BookResource als Contract-Test festhalten. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  2. Arbeitspaket 2
    BookRepository zunächst unverändert hinter BookService verwenden. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  3. Arbeitspaket 3
    BookController mit denselben Pfaden und Medienformaten einführen. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  4. Arbeitspaket 4
    Bean Validation und globale Fehlerabbildung ergänzen; Fehlerpayload versionieren. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  5. Arbeitspaket 5
    Legacy- und Modern-Endpunkt im Parallelbetrieb gegen dieselben Testvektoren ausführen. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  6. Arbeitspaket 6
    Traffic erst nach Contract-, Last- und Observability-Prüfung umschalten. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.

Konkreter Test- und Abnahmekatalog

IDEbenePrüfungerforderlicher Nachweis
03-REST-A01Integration/ContractGET und POST liefern erwartete Statuscodes und Content-Type.Automatisierter Test und CI-Protokoll
03-REST-A02Integration/ContractBestehende JSON-Clients funktionieren oder erhalten dokumentierte Versionierung.Automatisierter Test und CI-Protokoll
03-REST-A03Integration/ContractValidierungsfehler enthalten Feld, Code und verständliche Meldung.Automatisierter Test und CI-Protokoll
03-REST-A04Integration/ContractRepository-IDs werden nicht doppelt vergeben.Automatisierter Test und CI-Protokoll
03-REST-A05Integration/ContractActuator-Endpunkte sind nicht versehentlich öffentlich exponiert.Automatisierter Test und CI-Protokoll
03-REST-T01UnitFachlogik ohne Container oder externen Dienst testen.Unit-Test
03-REST-T02RegressionLegacy- und Modern-Ergebnis für denselben Golden-Master-Vektor vergleichen.Vergleichsreport
03-REST-T03NegativeFehlerhafte, leere und grenzwertige Eingaben prüfen.Negativtest
03-REST-T04OperationsStart, Health, Shutdown und Konfigurationsfehler prüfen.Deployment-/Startprotokoll
03-REST-T05ContractProtokoll, Status/Headers, Payload und Timeout gegen Consumer Contract prüfen.Contract-Test
03-REST-F01Fokusstatus codes, validation, error payload and actuator exposureModulspezifischer Testreport

Risikoregister des Moduls

RisikoAuswirkungGegenmaßnahmeGate
HTTP contract drift and validation gapsNIEDRIGstatus codes, validation, error payload and actuator exposurevor Cutover
Neue Framework-/Library-LaufzeitabhängigkeitenMITTELSBOM, Lizenz-, CVE- und transitive Dependency-Prüfung im Online-Buildvor Release
Legacy-Verhalten war implizit an entfernte Library gekoppeltMITTELContract-/Golden-Master-Tests und gezielte Fehlerpfadevor Abschaltung

Rollback und Koexistenz

Routing kann auf den JAX-RS-Endpunkt zurückgesetzt werden; Daten bleiben kompatibel, solange Book-Schema und ID-Regeln unverändert sind.

Abbruchkriterien

  • Fachlicher Golden-Master weicht ab.
  • Daten-, Nachrichten- oder API-Kompatibilität ist ungeklärt.
  • Fehlerquote, Latenz oder Ressourcenverbrauch überschreiten das vereinbarte Limit.
  • Monitoring oder Rückfallpfad ist nicht funktionsfähig.

Definition of Done

Direkte Arbeitslinks

⌂ Cockpit