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.
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.
| Dimension | Legacy | Modern | Migrationskonsequenz |
|---|---|---|---|
| Programmiermodell | Consumes, GET, POST, Path, Produces | GetMapping, NotBlank, PostMapping, Repository, RequestBody, RequestMapping, RestController, Service, SpringBootApplication, Valid | Annotationen und Containerfunktionen werden nur dort eingesetzt, wo sie eine konkrete technische Verantwortung übernehmen. |
| Abhängigkeiten | 4 direkte Dependencies | 4 direkte Dependencies | 4 neu, 4 entfernt; Versionen und transitive Auswirkungen im erfolgreichen Online-Build prüfen. |
| Öffentliche API | 10 erkannte Methoden | 7 erkannte Methoden | Methoden werden nach fachlicher Rolle gemappt; reine Bootstrap- und Framework-Methoden sind kein fachlicher Vertrag. |
| Datenmodell | class Book, class BookRepository, class BookResource, class RestLegacyApplication | class BookController, class BookRepository, class BookService, class RestModernApplication, record Book | Feldnamen, IDs, Null-Semantik, Gleichheit und Serialisierungsform werden separat regressionstestet. |
| Fehlerverhalten | Legacy-Exceptions und Rückgabewerte | explizitere Fach-/Framework-Fehlerabbildung | Fehler dürfen nicht nur technisch übersetzt werden; Status, Ursache, Retrybarkeit und Client-Vertrag müssen erhalten oder versioniert werden. |
| Tests | Bestands- und Golden-Master-Tests | Unit-, Slice-, Contract- und Integrationstests | Der Modern-Pfad wird zuerst gegen denselben fachlichen Vektor geprüft und danach um neue technische Risiken ergänzt. |
| Betrieb | Legacy-Start/Lifecycle | modernes Packaging, Health und externe Konfiguration | Parallelbetrieb, Replay/Retry und Rückrouting müssen vor Abschaltung des Legacy-Pfads erprobt sein. |
| Rollback | Legacy-Artefakt bleibt unverändert | Modern-Artefakt getrennt deploybar | Kein Rollback über Datenverlust: Schema, Nachrichten und verschlüsselte Daten müssen rückwärtslesbar oder durch Dual-Read abgesichert sein. |
HTTP contract drift and validation gaps
status codes, validation, error payload and actuator exposure
Die Zuordnung ist semantisch: Eine Legacy-Klasse kann in mehrere moderne Rollen zerlegt werden.
| Legacy-Rolle / Datei | Modern-Rolle / Datei | Bedeutung |
|---|---|---|
| BookResource.java | BookController.java + BookService.java | Resource-Verantwortung wird in HTTP-Adapter und Service Layer getrennt. |
| BookRepository.java | BookRepository.java | Datenzugriffsvertrag bleibt erhalten. |
| Book.java | Book.java | API-/Domain-Datenform bleibt vergleichbar. |
| RestLegacyApplication.java | RestModernApplication.java | Jersey/Grizzly-Bootstrap wird Spring Boot Bootstrap. |
| Datei | Typ | Annotationen | öffentliche API | Pattern | Zeilen |
|---|---|---|---|---|---|
| Book.java | class Book | — | long getId() void setId(long id) String getTitle() void setTitle(String title) | — | 29 |
| BookRepository.java | class BookRepository | — | BookRepository instance() List<Book> findAll() Book save(Book book) | Repository | 32 |
| BookResource.java | class BookResource | Consumes, GET, POST, Path, Produces | List<Book> all() Response create(Book book) | — | 28 |
| RestLegacyApplication.java | class RestLegacyApplication | — | void main(String[] args) | — | 21 |
| Datei | Typ | Annotationen | öffentliche API | Pattern | Zeilen |
|---|---|---|---|---|---|
| Book.java | record Book | NotBlank | — | — | 6 |
| BookController.java | class BookController | GetMapping, PostMapping, RequestBody, RequestMapping, RestController, Valid | List<Book> all() ResponseEntity<Book> create(@Valid @RequestBody Book book, UriComponentsBuilder uriBuilder) | — | 31 |
| BookRepository.java | class BookRepository | Repository | List<Book> findAll() Book save(Book input) | Repository | 28 |
| BookService.java | class BookService | Service | List<Book> findAll() Book create(Book book) | Service Layer | 22 |
| RestModernApplication.java | class RestModernApplication | SpringBootApplication | void main(String[] args) | — | 11 |
| Status | Dependency | Version | Scope | Prüfung |
|---|---|---|---|---|
| ENTFERNT | org.glassfish.jersey.containers:jersey-container-grizzly2-http | ${jersey.version} | compile | Legacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt. |
| ENTFERNT | org.glassfish.jersey.inject:jersey-hk2 | ${jersey.version} | compile | Legacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt. |
| ENTFERNT | org.glassfish.jersey.media:jersey-media-json-jackson | ${jersey.version} | compile | Legacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt. |
| ENTFERNT | org.junit.jupiter:junit-jupiter | BOM/Parent | test | Legacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt. |
| NEU | org.springframework.boot:spring-boot-starter-actuator | BOM/Parent | compile | Neue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen. |
| NEU | org.springframework.boot:spring-boot-starter-test | BOM/Parent | test | Neue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen. |
| NEU | org.springframework.boot:spring-boot-starter-validation | BOM/Parent | compile | Neue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen. |
| NEU | org.springframework.boot:spring-boot-starter-web | BOM/Parent | compile | Neue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen. |
Die folgenden Ausschnitte stammen direkt aus den enthaltenen Projekten. Dadurch ist sichtbar, welche Verantwortung tatsächlich verschoben wurde.
Migrationsbedeutung: Resource-Verantwortung wird in HTTP-Adapter und Service Layer getrennt.
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();
}
}
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);
}
}
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);
}
}
Migrationsbedeutung: Datenzugriffsvertrag bleibt erhalten.
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;
}
}
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;
}
}
--- 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;
}
Migrationsbedeutung: API-/Domain-Datenform bleibt vergleichbar.
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;
}
}
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) {
}
--- 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) {
}
Migrationsbedeutung: Jersey/Grizzly-Bootstrap wird Spring Boot Bootstrap.
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();
}
}
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);
}
}
--- 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);
}
}
| ID | Ebene | Prüfung | erforderlicher Nachweis |
|---|---|---|---|
| 03-REST-A01 | Integration/Contract | GET und POST liefern erwartete Statuscodes und Content-Type. | Automatisierter Test und CI-Protokoll |
| 03-REST-A02 | Integration/Contract | Bestehende JSON-Clients funktionieren oder erhalten dokumentierte Versionierung. | Automatisierter Test und CI-Protokoll |
| 03-REST-A03 | Integration/Contract | Validierungsfehler enthalten Feld, Code und verständliche Meldung. | Automatisierter Test und CI-Protokoll |
| 03-REST-A04 | Integration/Contract | Repository-IDs werden nicht doppelt vergeben. | Automatisierter Test und CI-Protokoll |
| 03-REST-A05 | Integration/Contract | Actuator-Endpunkte sind nicht versehentlich öffentlich exponiert. | Automatisierter Test und CI-Protokoll |
| 03-REST-T01 | Unit | Fachlogik ohne Container oder externen Dienst testen. | Unit-Test |
| 03-REST-T02 | Regression | Legacy- und Modern-Ergebnis für denselben Golden-Master-Vektor vergleichen. | Vergleichsreport |
| 03-REST-T03 | Negative | Fehlerhafte, leere und grenzwertige Eingaben prüfen. | Negativtest |
| 03-REST-T04 | Operations | Start, Health, Shutdown und Konfigurationsfehler prüfen. | Deployment-/Startprotokoll |
| 03-REST-T05 | Contract | Protokoll, Status/Headers, Payload und Timeout gegen Consumer Contract prüfen. | Contract-Test |
| 03-REST-F01 | Fokus | status codes, validation, error payload and actuator exposure | Modulspezifischer Testreport |
| Risiko | Auswirkung | Gegenmaßnahme | Gate |
|---|---|---|---|
| HTTP contract drift and validation gaps | NIEDRIG | status codes, validation, error payload and actuator exposure | vor Cutover |
| Neue Framework-/Library-Laufzeitabhängigkeiten | MITTEL | SBOM, Lizenz-, CVE- und transitive Dependency-Prüfung im Online-Build | vor Release |
| Legacy-Verhalten war implizit an entfernte Library gekoppelt | MITTEL | Contract-/Golden-Master-Tests und gezielte Fehlerpfade | vor Abschaltung |
| Seite | Ressource | Zeilen |
|---|---|---|
| Modern | modern/src/main/resources/application.properties | 2 |
Routing kann auf den JAX-RS-Endpunkt zurückgesetzt werden; Daten bleiben kompatibel, solange Book-Schema und ID-Regeln unverändert sind.