GEDOPLAN
Java

Java 24 Scoped Values: Werte mit Haltung

Java
coffee cup 2317201 1280

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:

  1. Kunden können Bestellungen aufgeben, mit Produkten und deren Gesamtbetrag.
  2. Die Bestellung wird verarbeitet und bezahlt.
  3. Eine Bestellbestätigung wird zugesendet.
  4. 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:

MerkmalMit Scoped ValuesOhne Scoped Values
Übergabe von traceId im StackNicht erforderlich, TRACE_ID.get() ist automatisch verfügbarMuss explizit bei jedem Methodenaufruf übergeben werden
Code-LesbarkeitKlarer, weniger ParameterUnübersichtlich, traceId muss in jeder Methode übergeben werden
MethodensignaturenKein traceId als Parameter nötigtraceId muss explizit übergeben werden
LoggingTRACE_ID.get() wird direkt verwendettraceId wird an jede Log-Methode übergeben
WartbarkeitLeichter wartbar durch weniger Boilerplate-CodeSchwerer wartbar, da alle Methodensignaturen wachsen
Optimierung für virtuelle ThreadsFunktioniert gut mit virtuellen Threads in Java 24Kann 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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

blockchain 8244279 640 jpg
zero

Angular, Directive Composition

Direktiven sind ein mächtiges Werkzeug bei der Entwicklung mit Angular, um diverse Problemstellungen generisch und wiederverwendbar zu implementieren. Ein Feature,…
IT-Training - GEDOPLAN
Jakarta EE (Java EE)

Lifecycle-Events der CDI-Scopes

Beim Aktivieren und Deaktivieren von CDI-Scopes sendet der Container Events, die von der Anwendung bspw. zur Initialisierung genutzt werden können.…

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!