#Jakarta-EE-Schwerpunkt: Enterprise-Beispielprojekt
Dieses Dokument ergänzt den Spring/Jakarta-Lernpfad um ein Jakarta-zentriertes Beispielprojekt. Der Fokus liegt nicht auf Spring Boot, sondern auf den Standards der Jakarta EE Platform: JAX-RS, CDI, JPA, JTA, Bean Validation, Jakarta Security, Jakarta Messaging und WAR-Deployment auf einem Application Server.
#Ziel des Beispiels
Das Beispielprojekt heißt Jakarta Retail Banking Platform. Es simuliert eine kleine Banking-Plattform mit mehreren deploybaren WAR-Modulen.
Lernziele:
- Jakarta EE als serverbasierte Enterprise-Plattform verstehen
- WAR-Module mit Maven strukturieren
- REST APIs mit JAX-RS bauen
- Fachlogik mit CDI Application Services kapseln
- Datenzugriff über JPA und
EntityManagerumsetzen - Transaktionen mit JTA steuern
- Validierung mit Bean Validation nutzen
- Rollen mit Jakarta Security und
@RolesAllowedabsichern - asynchrone Integration mit Jakarta Messaging und MDB erklären
- Application-Server-Ressourcen wie Datasource und JMS Queue bewusst modellieren
#1. Architekturüberblick
#1.1 Laufzeitmodell
API Client / Browser
|
+--> customer-app.war JAX-RS + CDI + JPA
|
+--> account-app.war JAX-RS + CDI + JPA
|
+--> transfer-app.war JAX-RS + CDI + JTA + JMS
|
v
PaymentEvents Queue
|
v
notification-app.war
MDB + JPA + REST Admin API
Anders als bei Spring Boot bringt hier nicht jede Anwendung ihren eingebetteten Server mit. Die WAR-Dateien werden auf einen Jakarta-EE-kompatiblen Application Server deployed. Der Server stellt HTTP Runtime, CDI Container, JPA Provider, JTA Transaction Manager, Security-Integration, Datasources und Messaging-Infrastruktur bereit.
#1.2 Module
| Modul | Typ | Verantwortung |
|---|---|---|
domain-kernel |
JAR | Value Objects wie Money, Iban, DomainEvent |
api-contracts |
JAR | DTOs und Event-Verträge |
customer-app |
WAR | Kundenverwaltung |
account-app |
WAR | Konten und Salden |
transfer-app |
WAR | Überweisungsprozess, Transaktion, JMS Event |
notification-app |
WAR | Message-Driven Bean und Notification Log |
#2. Projektstruktur
jakarta-retail-banking-platform/
├── pom.xml
├── domain-kernel/
├── api-contracts/
├── customer-app/
├── account-app/
├── transfer-app/
├── notification-app/
├── docker-compose.yml
├── deployments/wildfly/configure-wildfly.cli
├── docs/
└── scripts/smoke-test.sh
#2.1 Warum mehrere WARs?
Mehrere WARs zeigen ein Enterprise-Szenario, in dem fachliche Kontexte getrennt deployt werden. Das ist nicht zwingend eine Microservice-Architektur, aber ein guter Übergang zwischen klassischem Application-Server-Deployment und serviceorientierter Architektur.
Vorteile:
- getrennte Context Roots
- klare fachliche Grenzen
- gemeinsame Serverressourcen
- Thin-WAR-Deployment
- weniger Framework-spezifische Kopplung
Nachteile:
- mehr Serverkonfiguration
- Deployment-Reihenfolge und JNDI-Namen müssen stimmen
- lokale Entwicklung ist aufwendiger als bei Spring Boot
#3. Maven Parent-POM
Die Root-POM verwaltet Versionen, Module und Plugin-Standards. Die wichtigste Dependency ist jakarta.jakartaee-api mit Scope provided. Das bedeutet: Die APIs sind beim Kompilieren sichtbar, aber die Implementierung kommt zur Laufzeit vom Application Server.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme.banking</groupId>
<artifactId>jakarta-retail-banking-platform</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<name>Jakarta Retail Banking Platform</name>
<description>Jakarta-EE-Schwerpunktprojekt mit mehreren WAR-Modulen.</description>
<modules>
<module>domain-kernel</module>
<module>api-contracts</module>
<module>customer-app</module>
<module>account-app</module>
<module>transfer-app</module>
<module>notification-app</module>
</modules>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<java.version>17</java.version>
<jakartaee.version>10.0.0</jakartaee.version>
<junit.version>5.11.4</junit.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>${jakartaee.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>${java.version}</release>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>enforce-java-and-maven</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[17,)</version>
</requireJavaVersion>
<requireMavenVersion>
<version>[3.9,)</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
#3.1 Warum provided?
Bei Jakarta EE gehören viele Implementierungen zum Server. Wenn ein WAR seine eigenen Implementierungen für JPA, CDI, JAX-RS oder JMS mitbringt, kann es zu Classloading-Konflikten kommen. Deshalb werden die Jakarta APIs typischerweise mit provided eingebunden.
#3.2 Warum Jakarta EE 10 im Beispiel?
Das Beispiel nutzt Jakarta EE 10, weil dafür produktive Application-Server-Unterstützung breit verfügbar ist. Jakarta EE 11 ist wichtig für den Lernpfad, aber für ein möglichst nachvollziehbares Serverbeispiel ist Jakarta EE 10 derzeit pragmatischer.
#4. JAX-RS: REST Boundary
JAX-RS Resources bilden die äußere Grenze der Anwendung. Sie nehmen HTTP Requests entgegen, validieren DTOs und delegieren an CDI Services.
#4.1 TransferResource
package com.acme.banking.transfer.boundary;
import com.acme.banking.contracts.CreateTransferRequest;
import com.acme.banking.contracts.TransferResponse;
import com.acme.banking.transfer.control.TransferService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
@Path("/transfers")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class TransferResource {
@Inject
TransferService service;
@GET
@RolesAllowed({"USER", "ADMIN"})
public List<TransferResponse> all() {
return service.all();
}
@POST
@RolesAllowed("USER")
public Response request(@Valid CreateTransferRequest request) {
TransferResponse response = service.requestTransfer(request);
return Response.created(URI.create("/transfers/" + response.id())).entity(response).build();
}
}
Wichtige Punkte:
@Path("/transfers")definiert den REST-Pfad.@Consumesund@Producesdefinieren JSON als Ein- und Ausgabeformat.@Validaktiviert Bean Validation für das Request-DTO.@RolesAllowed("USER")schützt den Endpunkt deklarativ.- Die Resource enthält keine Geschäftslogik, sondern delegiert an
TransferService.
#5. CDI und JTA: Application Service
CDI verwaltet die Lebenszyklen der Services. JTA sorgt dafür, dass der Use Case transaktional ausgeführt wird.
#5.1 TransferService
package com.acme.banking.transfer.control;
import com.acme.banking.contracts.CreateTransferRequest;
import com.acme.banking.contracts.PaymentRequestedEvent;
import com.acme.banking.contracts.TransferResponse;
import com.acme.banking.transfer.entity.TransferEntity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@ApplicationScoped
public class TransferService {
@Inject
TransferRepository repository;
@Inject
PaymentEventPublisher publisher;
public List<TransferResponse> all() {
return repository.findAll().stream().map(this::toResponse).toList();
}
@Transactional
public TransferResponse requestTransfer(CreateTransferRequest request) {
TransferEntity transfer = TransferEntity.requested(
request.sourceAccountId(),
request.targetAccountId(),
request.amount(),
request.currency(),
request.reference()
);
repository.save(transfer);
transfer.markPaymentRequested();
publisher.publish(new PaymentRequestedEvent(
UUID.randomUUID(),
Instant.now(),
transfer.getId(),
transfer.getSourceAccountId(),
transfer.getTargetAccountId(),
transfer.getAmount(),
transfer.getCurrency(),
transfer.getReference()
));
return toResponse(transfer);
}
private TransferResponse toResponse(TransferEntity t) {
return new TransferResponse(
t.getId(), t.getSourceAccountId(), t.getTargetAccountId(),
t.getAmount(), t.getCurrency(), t.getReference(), t.getStatus(), t.getCreatedAt()
);
}
}
#5.2 Erklärung
Der Service macht mehrere Dinge in einer Transaktion:
- Er erstellt eine neue Überweisung.
- Er persistiert die Überweisung über JPA.
- Er ändert den Status.
- Er veröffentlicht ein JMS Event.
- Er gibt eine REST Response zurück.
@Transactional ist hier der zentrale Enterprise-Mechanismus. Wenn beim Persistieren oder Senden des Events ein Fehler auftritt, kann die Transaktion zurückgerollt werden.
#6. JPA: Persistence Context
JPA wird im Application Server integriert betrieben. Der EntityManager wird über @PersistenceContext injiziert.
#6.1 persistence.xml
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_1.xsd"
version="3.1">
<persistence-unit name="bankingPU" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/BankingDS</jta-data-source>
<properties>
<property name="jakarta.persistence.schema-generation.database.action" value="none" />
<property name="hibernate.default_schema" value="banking" />
</properties>
</persistence-unit>
</persistence>
#6.2 JNDI Datasource
Die Anwendung referenziert java:jboss/datasources/BankingDS. Diese Datasource wird nicht im WAR erstellt, sondern im Application Server. Genau das ist ein zentraler Jakarta-EE-Unterschied gegenüber vielen Spring-Boot-Projekten.
#7. Jakarta Messaging: Queue und MDB
Das Transfer-Modul sendet ein PaymentRequestedEvent an eine JMS Queue. Das Notification-Modul verarbeitet dieses Event asynchron mit einer Message-Driven Bean.
#7.1 PaymentEventConsumer
package com.acme.banking.notification.messaging;
import com.acme.banking.notification.control.NotificationLogRepository;
import com.acme.banking.notification.entity.NotificationLogEntity;
import jakarta.ejb.ActivationConfigProperty;
import jakarta.ejb.MessageDriven;
import jakarta.inject.Inject;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import jakarta.jms.TextMessage;
import jakarta.transaction.Transactional;
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "java:/jms/queue/PaymentEvents"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "jakarta.jms.Queue")
})
public class PaymentEventConsumer implements MessageListener {
@Inject
NotificationLogRepository repository;
@Override
@Transactional
public void onMessage(Message message) {
try {
String eventType = message.getStringProperty("eventType");
String payload = ((TextMessage) message).getText();
repository.save(NotificationLogEntity.received(eventType, payload));
} catch (Exception ex) {
throw new IllegalStateException("could not process payment event", ex);
}
}
}
#7.2 Warum MDB?
Eine Message-Driven Bean ist ein serververwalteter Consumer. Der Application Server kümmert sich um:
- Listener-Lifecycle
- Threading
- Verbindung zum Messaging Provider
- Transaktionsintegration
- Redelivery-Verhalten
Das ist besonders nützlich, wenn man klassische Enterprise-Messaging-Patterns lernen will.
#8. Jakarta Security
Das Beispiel enthält eine einfache Demo-Identity-Store-Implementierung. Sie ist nur für Lernzwecke gedacht.
package com.acme.banking.transfer.security;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.credential.Credential;
import jakarta.security.enterprise.credential.UsernamePasswordCredential;
import jakarta.security.enterprise.identitystore.CredentialValidationResult;
import jakarta.security.enterprise.identitystore.IdentityStore;
import java.util.Set;
@ApplicationScoped
public class DemoIdentityStore implements IdentityStore {
@Override
public CredentialValidationResult validate(Credential credential) {
if (credential instanceof UsernamePasswordCredential login) {
String user = login.getCaller();
String password = login.getPasswordAsString();
if ("admin".equals(user) && "admin".equals(password)) {
return new CredentialValidationResult(user, Set.of("ADMIN", "USER"));
}
if ("user".equals(user) && "user".equals(password)) {
return new CredentialValidationResult(user, Set.of("USER"));
}
}
return CredentialValidationResult.INVALID_RESULT;
}
}
#8.1 Rollenmodell
| Rolle | Bedeutung |
|---|---|
USER |
darf Überweisungen anlegen und Daten lesen |
ADMIN |
darf administrative Endpunkte nutzen |
In einer echten Enterprise-Anwendung würde man statt Hardcoding einen Identity Provider verwenden, zum Beispiel LDAP, OIDC, Keycloak oder eine vom Server verwaltete Security Realm.
#9. Application-Server-Konfiguration
#9.1 Serverressourcen
Für das Beispiel werden mindestens diese Ressourcen benötigt:
| Ressource | JNDI Name | Zweck |
|---|---|---|
| Datasource | java:jboss/datasources/BankingDS |
Verbindung zu PostgreSQL |
| JMS Queue | java:/jms/queue/PaymentEvents |
asynchrone Payment Events |
#9.2 WildFly CLI Auszug
/subsystem=datasources/data-source=BankingDS:add(
jndi-name=java:jboss/datasources/BankingDS,
driver-name=postgresql,
connection-url=jdbc:postgresql://localhost:5432/banking,
user-name=banking,
password=banking,
min-pool-size=2,
max-pool-size=20
)
jms-queue add --queue-address=PaymentEvents --entries=java:/jms/queue/PaymentEvents
#10. Build und Deployment
#10.1 Build
mvn clean package
#10.2 Infrastruktur starten
docker compose up -d postgres adminer
#10.3 WARs deployen
cp customer-app/target/customer-app.war $WILDFLY_HOME/standalone/deployments/
cp account-app/target/account-app.war $WILDFLY_HOME/standalone/deployments/
cp transfer-app/target/transfer-app.war $WILDFLY_HOME/standalone/deployments/
cp notification-app/target/notification-app.war $WILDFLY_HOME/standalone/deployments/
#10.4 Smoke Test
bash scripts/smoke-test.sh
#11. Vergleich zu Spring Boot
| Thema | Jakarta EE Beispiel | Spring Boot Beispiel |
|---|---|---|
| Runtime | externer Application Server | eingebetteter Server |
| Deployment | WAR | meist ausführbares JAR oder Container Image |
| Dependency Scope | Jakarta APIs meist provided |
Starter bringen Implementierungen mit |
| Transaktionen | JTA/serververwaltet | Spring Transaction Manager |
| Security | Jakarta Security / Server Realm | Spring Security |
| Messaging | Jakarta Messaging / MDB | Spring for Kafka, JMS, AMQP |
| Konfiguration | Server/JNDI + Deskriptoren | application.yml, Config Server, ENV |
| Betriebsmodell | zentraler Server | cloud-native App-Instanzen |
#12. Übungen
#12.1 Grundlagen
- Baue das Projekt mit
mvn clean package. - Öffne die Root-POM und finde alle Module.
- Erkläre, warum
jakarta.jakartaee-apinicht in das WAR gepackt werden sollte. - Suche alle Stellen, an denen
@Injectverwendet wird. - Suche alle Stellen, an denen
@Transactionalverwendet wird.
#12.2 REST und Validation
- Ergänze im
customer-appeinen EndpunktGET /customers/{id}. - Ergänze im
CreateTransferRequesteine maximale Referenzlänge. - Erzeuge absichtlich einen ungültigen Request und prüfe die Serverantwort.
#12.3 JPA
- Ergänze eine
NamedQuery, um aktive Konten zu suchen. - Füge ein Feld
createdAtzuCustomerEntityhinzu. - Erkläre, warum Schema-Migrationen in Produktion nicht über
schema-generationlaufen sollten.
#12.4 Messaging
- Ergänze ein zweites Event
TransferRejectedEvent. - Erweitere den Consumer so, dass er unterschiedliche
eventType-Werte verarbeitet. - Simuliere einen Fehler im Consumer und beobachte Redelivery-Verhalten des Servers.
#12.5 Security
- Ersetze die Demo-Identity-Store-Logik durch eine serververwaltete Realm-Konfiguration.
- Ergänze eine Rolle
AUDITOR, die nur lesen darf. - Prüfe, welche Endpunkte mit
ADMIN,USERund ohne Login erreichbar sind.
#13. Erweiterungsideen
#13.1 Jakarta Batch
Ein Batch-Modul könnte nächtliche Kontoauszüge erzeugen. Dazu würde man Jakarta Batch mit Job-XML und Batchlet/Chunk-Step einsetzen.
#13.2 Jakarta Mail
Das Notification-Modul könnte echte E-Mail-Benachrichtigungen versenden.
#13.3 Jakarta Concurrency
Für serververwaltete Hintergrundaufgaben kann Jakarta Concurrency verwendet werden, statt eigene Threads zu starten.
#13.4 Jakarta Faces
Ein optionales admin-console.war könnte mit Jakarta Faces eine klassische serverseitige Admin-Oberfläche bereitstellen.
#13.5 MicroProfile ergänzen
Für Cloud-native Anforderungen kann man später MicroProfile Config, Health, Metrics, OpenAPI, Fault Tolerance und JWT ergänzen. Das wäre dann ein Jakarta-EE-plus-MicroProfile-Projekt.
#14. Prüfungsfragen
- Warum ist
jakarta.jakartaee-apiim Scopeprovided? - Was ist der Unterschied zwischen
@Injectund@PersistenceContext? - Warum liegt die Geschäftslogik nicht direkt in der JAX-RS Resource?
- Welche Rolle spielt JTA bei mehreren Datenbankoperationen?
- Warum ist eine JMS Queue für asynchrone Integration geeignet?
- Was macht eine Message-Driven Bean anders als ein normaler CDI Service?
- Was bedeutet JNDI in diesem Projekt?
- Welche Risiken entstehen durch Hardcoded Demo-User?
- Warum braucht ein WAR-Projekt einen Application Server?
- Wann würdest du Spring Boot statt Jakarta EE wählen?
#15. Download-Inhalt
Das ZIP enthält:
jakarta-retail-banking-platform/
├── README.md
├── pom.xml
├── domain-kernel/
├── api-contracts/
├── customer-app/
├── account-app/
├── transfer-app/
├── notification-app/
├── docker-compose.yml
├── deployments/wildfly/configure-wildfly.cli
├── docs/architecture.md
├── docs/adr/0001-use-jakarta-ee-full-platform.md
├── docs/run-on-wildfly.md
└── scripts/smoke-test.sh
#16. Lernreihenfolge für dieses Beispiel
README.mdlesen- Root-
pom.xmlverstehen api-contractsunddomain-kernelanschauencustomer-appals einfachstes WAR analysierenaccount-appmit Entity-Logik analysierentransfer-appals transaktionalen Use Case verstehennotification-appund MDB nachvollziehen- Serverressourcen in
deployments/wildfly/configure-wildfly.cliprüfen - Übungen schrittweise umsetzen
- Optional MicroProfile ergänzen