Framework-Vergleichs-Lab

56 Java-Dateien. 9 zentrale Dateien werden direkt mit echtem Quellcode und ihrem Zusammenspiel erklärt.

Zurück zu Code-Labs

Kapitel 17 · Framework-Vergleich

Was dieses Lab zeigt

Legt Spring Boot, Jakarta EE, Quarkus und Micronaut als Adapter um denselben fachlichen Use Case. So werden Runtime-, DI- und HTTP-Unterschiede vergleichbar, ohne die Domäne zu vermischen.

Lernziele

  • Framework-Code isolieren
  • Adapter vergleichbar halten
  • Architektur unabhängig bewerten

Technik und Schwerpunkte

Java 2156 Java-Dateien1 Tests/RunnerSpring BootJakarta EEQuarkus / Micronaut
Echter Quellcode aus diesem Lab

Geführter Codepfad

Dasselbe fachliche Place-Order-Modell wird über Spring-, Jakarta-, Quarkus- und Micronaut-Adapter aufgerufen. Dadurch bleibt sichtbar, welcher Code Framework-spezifisch ist und welcher unverändert zum Kern gehört.

FrameworkDeepDiveDemoSpringBootOrderControllerJakartaOrderResourceQuarkusOrderResourceMicronautOrderControllerPlaceOrderUseCaseOrderSimpleTransactionTemplateFrameworkDeepDiveTestRunner
Lesereihenfolge der zentralen Klassen. Die Pfeile zeigen den didaktischen Weg durch den realen Quellcode, nicht zwingend jeden Laufzeitaufruf.
1. FrameworkDeepDiveDemoVerdrahtet den gemeinsamen Kern mit mehreren Framework-Adaptern und führt identische Requests aus.
2. SpringBootOrderControllerÜbersetzt den Spring-artigen HTTP-Vertrag in den gemeinsamen PlaceOrderUseCase.
3. JakartaOrderResourceZeigt denselben Adapter als Jakarta-Ressource.
4. QuarkusOrderResourceZeigt die Quarkus-Variante ohne Änderung des fachlichen Kerns.
5. MicronautOrderControllerZeigt die Micronaut-Variante mit derselben Use-Case-Schnittstelle.
6. PlaceOrderUseCaseOrchestriert Validierung, Zahlung, Repository, Outbox und Transaktion frameworkneutral.
7. OrderEnthält das gemeinsame Aggregat und seine fachlichen Zustandsübergänge.
8. SimpleTransactionTemplateImplementiert die im Lab verwendete Transaktionsabstraktion für alle Adapter.
9. FrameworkDeepDiveTestRunnerBeweist, dass alle Framework-Adapter denselben fachlichen Vertrag erfüllen.

1. FrameworkDeepDiveDemo

src/main/java/com/example/framework/deepdive/runtime/FrameworkDeepDiveDemo.java
Java-Datei öffnen
Rolle im Ablauf

Verdrahtet den gemeinsamen Kern mit mehreren Framework-Adaptern und führt identische Requests aus.

Im Lesepfad folgt SpringBootOrderController: Übersetzt den Spring-artigen HTTP-Vertrag in den gemeinsamen PlaceOrderUseCase.

Typ
class FrameworkDeepDiveDemo
Verwendet
SpringBootOrderController, PlaceOrderUseCase, SimpleTransactionTemplate
Verwendet von
Einstiege
main(String[] args)
package com.example.framework.deepdive.runtime;
import com.example.framework.deepdive.adapters.*;
import com.example.framework.deepdive.application.*;
import com.example.framework.deepdive.frameworks.spring.*;
import java.time.Clock;
import java.util.List;
public final class FrameworkDeepDiveDemo {
    public static void main(String[] args) {
        var orders = new InMemoryOrderRepository(); var outbox = new InMemoryOutbox(); var tx = new SimpleTransactionTemplate();
        var useCase = new PlaceOrderUseCase(orders, new ApprovingPaymentAdapter(), outbox, tx, new OrderNumberGenerator(), Clock.systemUTC());
        var spring = new SpringBootOrderController(useCase);
        var response = spring.place(new PlaceOrderHttpRequest("REQ-1", "C-42", List.of(new OrderLineDto("SKU-1", 2, "19.90"))));
        System.out.println("RUN24_DEMO_OK status=" + response.status() + " orders=" + orders.size() + " outbox=" + outbox.pendingCount());
    }
}

2. SpringBootOrderController

src/main/java/com/example/framework/deepdive/frameworks/spring/SpringBootOrderController.java
Java-Datei öffnen
Rolle im Ablauf

Übersetzt den Spring-artigen HTTP-Vertrag in den gemeinsamen PlaceOrderUseCase.

Im Lesepfad folgt JakartaOrderResource: Zeigt denselben Adapter als Jakarta-Ressource.

Typ
class SpringBootOrderController
Verwendet
PlaceOrderUseCase
Verwendet von
FrameworkDeepDiveDemo, FrameworkDeepDiveTestRunner
Einstiege
place(PlaceOrderHttpRequest request)
Adapter
package com.example.framework.deepdive.frameworks.spring;
import com.example.framework.deepdive.application.PlaceOrderUseCase;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.fakeannotations.RestController;
import java.util.*;

@RestController("orders")
// Pattern: Adapter - uebersetzt HTTP DTOs in den frameworkfreien Use Case.
public final class SpringBootOrderController {
    private final PlaceOrderUseCase useCase;
    public SpringBootOrderController(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    @com.example.framework.deepdive.fakeannotations.PostMapping("/orders")
    public HttpResponse place(PlaceOrderHttpRequest request) {
        var command = new PlaceOrderCommand(request.requestId(), new CustomerId(request.customerId()), request.lines().stream().map(l -> new OrderLine(new Sku(l.sku()), l.quantity(), Money.eur(l.unitPrice()))).toList());
        return switch (useCase.place(command)) {
            case PlaceOrderResult.Accepted ok -> new HttpResponse(201, "accepted:" + ok.orderId().value() + ":" + ok.total());
            case PlaceOrderResult.Rejected bad -> new HttpResponse(statusFor(bad.error()), "problem:" + bad.error().code() + ":" + bad.error().message());
        };
    }
    private int statusFor(BusinessError error) {
        return switch (error) { case ValidationError e -> 400; case DuplicateRequest e -> 409; case PaymentDeclined e -> 422; };
    }
}

3. JakartaOrderResource

src/main/java/com/example/framework/deepdive/frameworks/jakarta/JakartaOrderResource.java
Java-Datei öffnen
Rolle im Ablauf

Zeigt denselben Adapter als Jakarta-Ressource.

Im Lesepfad folgt QuarkusOrderResource: Zeigt die Quarkus-Variante ohne Änderung des fachlichen Kerns.

Typ
class JakartaOrderResource
Verwendet
PlaceOrderUseCase
Verwendet von
FrameworkDeepDiveTestRunner
Einstiege
place(PlaceOrderHttpRequest request)
Adapter
package com.example.framework.deepdive.frameworks.jakarta;
import com.example.framework.deepdive.application.PlaceOrderUseCase;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.fakeannotations.Path;
import java.util.*;

@Path("/orders")
// Pattern: Adapter - uebersetzt HTTP DTOs in den frameworkfreien Use Case.
public final class JakartaOrderResource {
    private final PlaceOrderUseCase useCase;
    public JakartaOrderResource(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    @com.example.framework.deepdive.fakeannotations.POST
    public HttpResponse place(PlaceOrderHttpRequest request) {
        var command = new PlaceOrderCommand(request.requestId(), new CustomerId(request.customerId()), request.lines().stream().map(l -> new OrderLine(new Sku(l.sku()), l.quantity(), Money.eur(l.unitPrice()))).toList());
        return switch (useCase.place(command)) {
            case PlaceOrderResult.Accepted ok -> new HttpResponse(201, "accepted:" + ok.orderId().value() + ":" + ok.total());
            case PlaceOrderResult.Rejected bad -> new HttpResponse(statusFor(bad.error()), "problem:" + bad.error().code() + ":" + bad.error().message());
        };
    }
    private int statusFor(BusinessError error) {
        return switch (error) { case ValidationError e -> 400; case DuplicateRequest e -> 409; case PaymentDeclined e -> 422; };
    }
}

4. QuarkusOrderResource

src/main/java/com/example/framework/deepdive/frameworks/quarkus/QuarkusOrderResource.java
Java-Datei öffnen
Rolle im Ablauf

Zeigt die Quarkus-Variante ohne Änderung des fachlichen Kerns.

Im Lesepfad folgt MicronautOrderController: Zeigt die Micronaut-Variante mit derselben Use-Case-Schnittstelle.

Typ
class QuarkusOrderResource
Verwendet
PlaceOrderUseCase
Verwendet von
FrameworkDeepDiveTestRunner
Einstiege
place(PlaceOrderHttpRequest request)
Adapter
package com.example.framework.deepdive.frameworks.quarkus;
import com.example.framework.deepdive.application.PlaceOrderUseCase;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.fakeannotations.Path;
import java.util.*;

@Path("/orders")
// Pattern: Adapter - uebersetzt HTTP DTOs in den frameworkfreien Use Case.
public final class QuarkusOrderResource {
    private final PlaceOrderUseCase useCase;
    public QuarkusOrderResource(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    @com.example.framework.deepdive.fakeannotations.POST
    public HttpResponse place(PlaceOrderHttpRequest request) {
        var command = new PlaceOrderCommand(request.requestId(), new CustomerId(request.customerId()), request.lines().stream().map(l -> new OrderLine(new Sku(l.sku()), l.quantity(), Money.eur(l.unitPrice()))).toList());
        return switch (useCase.place(command)) {
            case PlaceOrderResult.Accepted ok -> new HttpResponse(201, "accepted:" + ok.orderId().value() + ":" + ok.total());
            case PlaceOrderResult.Rejected bad -> new HttpResponse(statusFor(bad.error()), "problem:" + bad.error().code() + ":" + bad.error().message());
        };
    }
    private int statusFor(BusinessError error) {
        return switch (error) { case ValidationError e -> 400; case DuplicateRequest e -> 409; case PaymentDeclined e -> 422; };
    }
}

5. MicronautOrderController

src/main/java/com/example/framework/deepdive/frameworks/micronaut/MicronautOrderController.java
Java-Datei öffnen
Rolle im Ablauf

Zeigt die Micronaut-Variante mit derselben Use-Case-Schnittstelle.

Im Lesepfad folgt PlaceOrderUseCase: Orchestriert Validierung, Zahlung, Repository, Outbox und Transaktion frameworkneutral.

Typ
class MicronautOrderController
Verwendet
PlaceOrderUseCase
Verwendet von
FrameworkDeepDiveTestRunner
Einstiege
place(PlaceOrderHttpRequest request)
Adapter
package com.example.framework.deepdive.frameworks.micronaut;
import com.example.framework.deepdive.application.PlaceOrderUseCase;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.fakeannotations.Controller;
import java.util.*;

@Controller("/orders")
// Pattern: Adapter - uebersetzt HTTP DTOs in den frameworkfreien Use Case.
public final class MicronautOrderController {
    private final PlaceOrderUseCase useCase;
    public MicronautOrderController(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    @com.example.framework.deepdive.fakeannotations.PostMapping("/")
    public HttpResponse place(PlaceOrderHttpRequest request) {
        var command = new PlaceOrderCommand(request.requestId(), new CustomerId(request.customerId()), request.lines().stream().map(l -> new OrderLine(new Sku(l.sku()), l.quantity(), Money.eur(l.unitPrice()))).toList());
        return switch (useCase.place(command)) {
            case PlaceOrderResult.Accepted ok -> new HttpResponse(201, "accepted:" + ok.orderId().value() + ":" + ok.total());
            case PlaceOrderResult.Rejected bad -> new HttpResponse(statusFor(bad.error()), "problem:" + bad.error().code() + ":" + bad.error().message());
        };
    }
    private int statusFor(BusinessError error) {
        return switch (error) { case ValidationError e -> 400; case DuplicateRequest e -> 409; case PaymentDeclined e -> 422; };
    }
}

6. PlaceOrderUseCase

src/main/java/com/example/framework/deepdive/application/PlaceOrderUseCase.java
Java-Datei öffnen
Rolle im Ablauf

Orchestriert Validierung, Zahlung, Repository, Outbox und Transaktion frameworkneutral.

Im Lesepfad folgt Order: Enthält das gemeinsame Aggregat und seine fachlichen Zustandsübergänge.

Typ
class PlaceOrderUseCase
Verwendet
Order
Verwendet von
FrameworkDeepDiveDemo, SpringBootOrderController, JakartaOrderResource, QuarkusOrderResource, MicronautOrderController, FrameworkDeepDiveTestRunner
Einstiege
place(PlaceOrderCommand command)
Application Service
package com.example.framework.deepdive.application;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.ports.*;
import java.time.Clock;
import java.util.Objects;
// Pattern: Application Service - orchestriert Ports, transaktionale Grenze und Aggregate.
public final class PlaceOrderUseCase {
    private final OrderRepository orders; private final PaymentPort payments; private final OutboxPort outbox; private final TransactionTemplate tx; private final OrderNumberGenerator numbers; private final Clock clock;
    public PlaceOrderUseCase(OrderRepository orders, PaymentPort payments, OutboxPort outbox, TransactionTemplate tx, OrderNumberGenerator numbers, Clock clock) {
        this.orders = Objects.requireNonNull(orders); this.payments = Objects.requireNonNull(payments); this.outbox = Objects.requireNonNull(outbox); this.tx = Objects.requireNonNull(tx); this.numbers = Objects.requireNonNull(numbers); this.clock = Objects.requireNonNull(clock);
    }
    public PlaceOrderResult place(PlaceOrderCommand command) {
        return tx.inTransaction(() -> {
            if (orders.findByRequestId(command.requestId()).isPresent()) return new PlaceOrderResult.Rejected(new DuplicateRequest("request already processed"));
            if (command.lines().isEmpty()) return new PlaceOrderResult.Rejected(new ValidationError("order must contain at least one line"));
            var order = Order.draft(numbers.next(), command.customerId(), command.lines());
            var payment = payments.authorize(command.customerId(), order.total());
            if (payment instanceof PaymentDecision.Declined declined) { order.reject(declined.reason(), clock); orders.save(command.requestId(), order); outbox.appendAll(order.pullEvents()); return new PlaceOrderResult.Rejected(new PaymentDeclined(declined.reason())); }
            order.accept(clock); orders.save(command.requestId(), order); outbox.appendAll(order.pullEvents()); return new PlaceOrderResult.Accepted(order.id(), order.total());
        });
    }
}

7. Order

src/main/java/com/example/framework/deepdive/domain/Order.java
Java-Datei öffnen
Rolle im Ablauf

Enthält das gemeinsame Aggregat und seine fachlichen Zustandsübergänge.

Im Lesepfad folgt SimpleTransactionTemplate: Implementiert die im Lab verwendete Transaktionsabstraktion für alle Adapter.

Typ
class Order
Verwendet
Verwendet von
PlaceOrderUseCase
Einstiege
draft(OrderId id, CustomerId customerId, List<OrderLine> lines), accept(Clock clock), reject(String reason, Clock clock), total(), id()
Aggregate Root
package com.example.framework.deepdive.domain;
import java.time.Clock;
import java.util.*;
// Pattern: Aggregate Root - Order schuetzt Invarianten und erzeugt Domain Events.
public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderLine> lines;
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderState state = OrderState.DRAFT;
    private Order(OrderId id, CustomerId customerId, List<OrderLine> lines) {
        this.id = Objects.requireNonNull(id); this.customerId = Objects.requireNonNull(customerId);
        if (lines == null || lines.isEmpty()) throw new IllegalArgumentException("at least one line required");
        this.lines = List.copyOf(lines);
    }
    public static Order draft(OrderId id, CustomerId customerId, List<OrderLine> lines) { return new Order(id, customerId, lines); }
    public void accept(Clock clock) { this.state = OrderState.ACCEPTED; events.add(new OrderAcceptedEvent(id, customerId, total(), clock.instant())); }
    public void reject(String reason, Clock clock) { this.state = OrderState.REJECTED; events.add(new OrderRejectedEvent(id, customerId, reason, clock.instant())); }
    public Money total() { return lines.stream().map(OrderLine::subtotal).reduce(Money.eur("0.00"), Money::add); }
    public OrderId id() { return id; } public CustomerId customerId() { return customerId; } public List<OrderLine> lines() { return lines; } public OrderState state() { return state; }
    public List<DomainEvent> pullEvents() { var copy = List.copyOf(events); events.clear(); return copy; }
}

8. SimpleTransactionTemplate

src/main/java/com/example/framework/deepdive/adapters/SimpleTransactionTemplate.java
Java-Datei öffnen
Rolle im Ablauf

Implementiert die im Lab verwendete Transaktionsabstraktion für alle Adapter.

Im Lesepfad folgt FrameworkDeepDiveTestRunner: Beweist, dass alle Framework-Adapter denselben fachlichen Vertrag erfüllen.

Typ
class SimpleTransactionTemplateimplements TransactionTemplate
Verwendet
Verwendet von
FrameworkDeepDiveDemo, FrameworkDeepDiveTestRunner
Einstiege
package com.example.framework.deepdive.adapters;
import com.example.framework.deepdive.ports.TransactionTemplate;
import java.util.function.Supplier;
public final class SimpleTransactionTemplate implements TransactionTemplate { private int commits; public <T> T inTransaction(Supplier<T> work) { T result = work.get(); commits++; return result; } public int commits() { return commits; } }

9. FrameworkDeepDiveTestRunner

src/test/java/com/example/framework/deepdive/FrameworkDeepDiveTestRunner.java
Java-Datei öffnen
Rolle im Ablauf

Beweist, dass alle Framework-Adapter denselben fachlichen Vertrag erfüllen.

Damit ist der zentrale Pfad abgeschlossen; der Test-/Runner-Code und die vollständige Dateiliste darunter zeigen die übrigen Varianten.

Typ
class FrameworkDeepDiveTestRunner
Verwendet
SpringBootOrderController, JakartaOrderResource, QuarkusOrderResource, MicronautOrderController, PlaceOrderUseCase, SimpleTransactionTemplate
Verwendet von
Einstiege
main(String[] args)
package com.example.framework.deepdive;
import com.example.framework.deepdive.adapters.*;
import com.example.framework.deepdive.application.*;
import com.example.framework.deepdive.domain.*;
import com.example.framework.deepdive.frameworks.spring.*;
import com.example.framework.deepdive.frameworks.jakarta.*;
import com.example.framework.deepdive.frameworks.quarkus.*;
import com.example.framework.deepdive.frameworks.micronaut.*;
import java.time.Clock;
import java.util.List;
public final class FrameworkDeepDiveTestRunner {
    public static void main(String[] args) {
        sameUseCaseContractForEveryFramework(); duplicateRequestIsConflict(); declinedPaymentIsBusinessProblem();
        System.out.println("RUN24_TESTS_OK");
    }
    static PlaceOrderUseCase newUseCase(ApprovingPaymentAdapter payment, InMemoryOrderRepository orders, InMemoryOutbox outbox) { return new PlaceOrderUseCase(orders, payment, outbox, new SimpleTransactionTemplate(), new OrderNumberGenerator(), Clock.systemUTC()); }
    static void sameUseCaseContractForEveryFramework() {
        var orders = new InMemoryOrderRepository(); var outbox = new InMemoryOutbox(); var uc = newUseCase(new ApprovingPaymentAdapter(), orders, outbox);
        var spring = new SpringBootOrderController(uc).place(new com.example.framework.deepdive.frameworks.spring.PlaceOrderHttpRequest("REQ-S", "C-1", List.of(new com.example.framework.deepdive.frameworks.spring.OrderLineDto("SKU", 1, "10.00"))));
        var jakarta = new JakartaOrderResource(uc).place(new com.example.framework.deepdive.frameworks.jakarta.PlaceOrderHttpRequest("REQ-J", "C-1", List.of(new com.example.framework.deepdive.frameworks.jakarta.OrderLineDto("SKU", 1, "10.00"))));
        var quarkus = new QuarkusOrderResource(uc).place(new com.example.framework.deepdive.frameworks.quarkus.PlaceOrderHttpRequest("REQ-Q", "C-1", List.of(new com.example.framework.deepdive.frameworks.quarkus.OrderLineDto("SKU", 1, "10.00"))));
        var micronaut = new MicronautOrderController(uc).place(new com.example.framework.deepdive.frameworks.micronaut.PlaceOrderHttpRequest("REQ-M", "C-1", List.of(new com.example.framework.deepdive.frameworks.micronaut.OrderLineDto("SKU", 1, "10.00"))));
        assertEquals(201, spring.status()); assertEquals(201, jakarta.status()); assertEquals(201, quarkus.status()); assertEquals(201, micronaut.status()); assertEquals(4, orders.size()); assertEquals(4, outbox.pendingCount());
    }
    static void duplicateRequestIsConflict() {
        var orders = new InMemoryOrderRepository(); var outbox = new InMemoryOutbox(); var uc = newUseCase(new ApprovingPaymentAdapter(), orders, outbox); var c = new SpringBootOrderController(uc);
        var req = new com.example.framework.deepdive.frameworks.spring.PlaceOrderHttpRequest("REQ-DUP", "C-1", List.of(new com.example.framework.deepdive.frameworks.spring.OrderLineDto("SKU", 1, "10.00")));
        assertEquals(201, c.place(req).status()); assertEquals(409, c.place(req).status());
    }
    static void declinedPaymentIsBusinessProblem() {
        var orders = new InMemoryOrderRepository(); var outbox = new InMemoryOutbox(); var uc = new PlaceOrderUseCase(orders, new DecliningPaymentAdapter(), outbox, new SimpleTransactionTemplate(), new OrderNumberGenerator(), Clock.systemUTC());
        var c = new SpringBootOrderController(uc); var r = c.place(new com.example.framework.deepdive.frameworks.spring.PlaceOrderHttpRequest("REQ-DEC", "C-1", List.of(new com.example.framework.deepdive.frameworks.spring.OrderLineDto("SKU", 1, "10.00"))));
        assertEquals(422, r.status()); assertEquals(1, orders.size()); assertEquals(1, outbox.pendingCount());
    }
    static void assertEquals(Object e, Object a) { if (!e.equals(a)) throw new AssertionError("expected " + e + " but got " + a); }
}
Alle Projektdateien öffnen (60 Einträge)
⌂ Cockpit