Mit JEP 487 „Scoped Values“ führt Java eine neue Möglichkeit ein, Kontextwerte kontrolliert und threadsicher weiterzugeben – ohne ThreadLocal und ohne Parameter-Overhead.
Sie kennen das vielleicht: Ein REST-Endpoint ruft zwei Services auf, die wiederum jeweils einen weiteren Service, ein paar Repositories sind auch beteiligt, und alle möchten auch noch etwas loggen. Keiner benötigt alle Informationen aus dem Request, aber alle Informationen werden irgendwo benötigt.
Klassische Lösungsansätze
ThreadLocal-Variablen waren bisher die Standardlösung, um Kontextinformationen ohne Parameter-Weitergabe durch die Aufrufkette zu transportieren. Sie bringen jedoch erhebliche Nachteile mit sich: Sie sind mutable, können Memory-Leaks verursachen und sind in modernen asynchronen und reaktiven Programmiermodellen problematisch.
Eine Alternative besteht darin, die Kontextwerte einfach als Parameter durch die gesamte Aufrufkette zu reichen. Dies führt jedoch schnell zu überladenen Methodensignaturen und „Parameter-Tunneling“, bei dem viele Zwischenmethoden Parameter nur durchreichen, ohne sie selbst zu nutzen.
Auftritt Scoped Values
Scoped Values lösen dieses Problem elegant: Sie kombinieren die Vorteile der Parameterübergabe mit der Bequemlichkeit von ThreadLocals, aber ohne deren Nachteile. Sie sind immutable, haben einen klar definierten Gültigkeitsbereich und lassen sich obendrein ausgezeichnet zusammen mit virtuellen Threads und Structured Concurrency nutzen.
Beispiel: Bestellsystem
Schauen wir uns ein einfaches Beispiel an.
Wir haben ein Bestellsystem, das einen einfachen Online-Einkaufsprozess demonstriert. Das System bietet folgende Funktionalitäten:
- Kunden können Bestellungen aufgeben, mit Produkten und deren Gesamtbetrag.
- Die Bestellung wird verarbeitet und bezahlt.
- Eine Bestellbestätigung wird zugesendet.
- Der Kaufvorgang wird für Analysezwecke erfasst.
Die Anwendung besteht aus mehreren Services, die diese typische Bestellverarbeitung abbilden.
Zuerst werden die Kontextvariablen deklariert, in denen die benötigten Daten vorgehalten werden können:
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
private static final ScopedValue<String> SESSION_ID = ScopedValue.newInstance();
Im REST-Endpoint wird der Kontext einmalig initialisiert.
@POST
@Path("/scoped/orders")
public Response placeOrderScoped(OrderRequest orderRequest) throws Exception {
String requestId = UUID.randomUUID().toString();
String userId = orderRequest.userId;
String sessionId = orderRequest.sessionId;
// Set up context with Scoped Values
return ScopedValue
.where(REQUEST_ID, requestId)
.where(USER_ID, userId)
.where(SESSION_ID, sessionId)
.call(() -> {
...
Der OrderService nimmt neue Bestellungen entgegen und erstellt eine Bestellnummer. Er benötigt die Benutzer-ID für Zuordnungen und gibt die Bestellnummer an den PaymentService weiter.
Er kann mit USER_ID.get() diret auf den Kontext zugreifen:
public String createOrderScoped(String[] items, double amount) {
// Can access userId directly from ScopedValue
String userId = USER_ID.get();
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
System.out.printf("Creating order %s for user %s with %d items%n",
orderId, userId, items.length);
return orderId;
}
Der PaymentService verarbeitet Zahlungen für die Bestellung. Auch er benötigt die Benutzer-ID für Abrechnung und Sicherheitsüberprüfungen:
public boolean processPaymentScoped(String orderId, double amount) {
// Can access userId directly from ScopedValue
String userId = USER_ID.get();
System.out.printf("Processing payment of $%.2f for order %s (user: %s)%n",
amount, orderId, userId);
return true;
}
Der AuditService protokolliert abgeschlossene Transaktionen mit vollständigen Kontextinformationen für Compliance-Zwecke, die benötigten Daten kann er sich aus dem Kontext holen:
public void logTransactionScoped(String orderId, double amount) {
// Can access both userId and requestId directly
String userId = USER_ID.get();
String requestId = REQUEST_ID.get();
System.out.printf("AUDIT [%s]: User %s completed transaction %s for $%.2f%n",
requestId, userId, orderId, amount);
}
Der NotificationService sendet Bestätigungen an den Kunden, wofür er die Benutzer-ID und die Sitzungs-ID benötigt, beide stehen im Kontext zur Verfügung:
public void sendConfirmationScoped(String orderId) {
// Can access all context directly, even deep in the call hierarchy
String userId = USER_ID.get();
String sessionId = SESSION_ID.get();
System.out.printf("Sending confirmation to user %s for order %s%n", userId, orderId);
System.out.printf("Using session %s for notification customization%n", sessionId);
}
Der AnalyticsService erfasst Kaufdaten für Business Intelligence. Für die Analyse des Nutzerverhaltens werden beide Kontextinformationen benötigt, Benutzer-ID und Sitzungs-ID. Dieser Service demonstriert besonders gut, wie der Kontext Serviceebenen überspringen kann.
public void trackPurchaseScoped(String orderId) {
// Can access both userId and sessionId directly without them being passed through intermediary services
String userId = USER_ID.get();
String sessionId = SESSION_ID.get();
// In a real application, look up the order details from a repository
BigDecimal amount = lookupOrderAmount(orderId);
System.out.printf("ANALYTICS: Purchase in session %s by user %s: Order %s ($%.2f)%n",
sessionId, userId, orderId, amount);
}
Der Kontrollfluss mit Scoped Values ist bemerkenswert elegant: Die Benutzer- und Sitzungs-ID werden einmalig im REST-Endpunkt gesetzt und stehen dann automatisch allen nachgelagerten Services zur Verfügung, ohne sie durch alle Methodensignaturen durchzureichen. Besonders deutlich wird der Vorteil beim AnalyticsService, der auf die Sitzungs-ID zugreift, obwohl diese durch mehrere Service-Ebenen „hindurchspringt“, die sie selbst nicht verwenden. Dieser Ansatz vereinfacht Methodensignaturen und macht den Code wartbarer, da Kontext dort verfügbar ist, wo er benötigt wird – ohne ihn explizit weiterreichen zu müssen.
Vergleich ThreadLocal und Scoped Values
ThreadLocal-Ansatz (Traditionell)
In der traditionellen Version der Anwendung müssen alle Kontexte (wie requestId
, userId und
sessionId
) explizit mit ThreadLocal
-Variablen gespeichert werden. Diese Werte müssen dann manuell durch jede Methode weitergegeben werden, die sie benötigt. Dabei gibt es ein gewisses Risiko, dass Kontexte vergessen oder überschrieben werden. Am Ende jeder Anfrage müssen die ThreadLocal
-Werte manuell bereinigt werden.
Scoped Values
Mit Scoped Values können Kontexte automatisch durch die gesamte Anrufhierarchie propagiert werden, ohne dass sie explizit durch jede Methode weitergegeben werden müssen. Das erleichtert das Arbeiten mit Kontextinformationen und reduziert den Overhead der manuellen Kontextübergabe.
- ScopedValue.newInstance() erstellt eine neue Instanz von Scoped Values.
- Mit ScopedValue.where() können die Kontextwerte gesetzt werden, die dann automatisch durch alle nachfolgenden Methodenaufrufe propagiert werden.
- Der Kontext wird an alle Methoden weitergegeben, ohne dass sie als Parameter explizit übergeben werden müssen.
- Es gibt keine Notwendigkeit mehr,
ThreadLocal
zu verwenden oder die Kontexte manuell zu entfernen.
Was bringt’s?
Hier noch einmal die wesentlichen Vorteile beim Einsatz von Scoped Values im Überblick:
Merkmal | Mit Scoped Values | Ohne Scoped Values |
---|---|---|
Übergabe von traceId im Stack | Nicht erforderlich, TRACE_ID.get() ist automatisch verfügbar | Muss explizit bei jedem Methodenaufruf übergeben werden |
Code-Lesbarkeit | Klarer, weniger Parameter | Unübersichtlich, traceId muss in jeder Methode übergeben werden |
Methodensignaturen | Kein traceId als Parameter nötig | traceId muss explizit übergeben werden |
Logging | TRACE_ID.get() wird direkt verwendet | traceId wird an jede Log-Methode übergeben |
Wartbarkeit | Leichter wartbar durch weniger Boilerplate-Code | Schwerer wartbar, da alle Methodensignaturen wachsen |
Optimierung für virtuelle Threads | Funktioniert gut mit virtuellen Threads in Java 24 | Kann problematisch sein mit ThreadLocal oder manueller Übergabe |
Fazit
Mit Scoped Values entfällt lästiges Parameterweiterreichen und Methodensignaturen bleiben schlank und auf ihre eigentliche Geschäftslogik fokussiert. Während wir früher ThreadLocal nutzen mussten (mit manueller Bereinigung und Thread-Vererbungsproblemen), bietet Java 24 mit Scoped Values eine saubere, strukturierte Alternative mit klarem Gültigkeitsbereich, was die Wartbarkeit und Lesbarkeit des Codes erheblich verbessert.