Java 25 : Scoped Values (JEP 506)
Avec Java 25, la JEP 506 finalise les Scoped Values,
une nouvelle API pour partager des données immuables entre méthodes sans les passer explicitement
en paramètres. Cette fonctionnalité est une alternative moderne et performante à ThreadLocal,
spécialement conçue pour fonctionner efficacement avec les Virtual Threads.
Cette nouveauté fait partie des JEPs (Java Enhancement Proposals) finalisées dans Java 25.
Pourquoi remplacer ThreadLocal ?
ThreadLocal existe depuis Java 1.2 et permet de stocker des données spécifiques
à un thread. Cependant, cette API présente plusieurs problèmes :
Problèmes de ThreadLocal
- Mutabilité : Les valeurs peuvent être modifiées à tout moment, rendant le comportement difficile à prédire
-
Fuites mémoire : Si
remove()n'est pas appelé, les valeurs restent en mémoire, particulièrement problématique avec les pools de threads -
Héritage complexe :
InheritableThreadLocalcopie les valeurs aux threads enfants, ce qui est coûteux avec des milliers de Virtual Threads - Durée de vie illimitée : Une valeur ThreadLocal vit tant que le thread existe
// Problèmes typiques avec ThreadLocal
public class ExempleThreadLocal {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void handleRequest(User user) {
currentUser.set(user);
try {
processRequest();
} finally {
currentUser.remove(); // Facile à oublier !
}
}
// Avec Virtual Threads, chaque requête crée un nouveau thread
// → Des milliers de copies avec InheritableThreadLocal
}
Introduction aux Scoped Values
Les ScopedValue résolvent ces problèmes en offrant :
- Immuabilité : Une fois définie, la valeur ne peut pas être modifiée
- Durée de vie définie : La valeur n'existe que dans un scope précis
- Pas de fuites mémoire : Nettoyage automatique à la sortie du scope
- Performances optimisées : Conçues pour les Virtual Threads
Exemple de base
import java.lang.ScopedValue;
public class ExempleScopedValue {
// Déclaration d'une ScopedValue (comme une constante)
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(User user) {
// Création d'un scope avec la valeur
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// Dans ce bloc, CURRENT_USER.get() retourne user
processRequest();
validatePermissions();
saveAuditLog();
});
// Ici, CURRENT_USER n'est plus disponible
}
private void processRequest() {
// Accès à la valeur dans le scope
User user = CURRENT_USER.get();
System.out.println("Traitement pour: " + user.getName());
}
private void validatePermissions() {
User user = CURRENT_USER.get();
if (!user.hasPermission("READ")) {
throw new SecurityException("Permission refusée");
}
}
private void saveAuditLog() {
User user = CURRENT_USER.get();
AuditLogger.log("Action par " + user.getId());
}
}
API ScopedValue en détail
Création d'une ScopedValue
// Déclaration typique (static final)
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
private static final ScopedValue<Transaction> TRANSACTION = ScopedValue.newInstance();
Exécution dans un scope
// Avec runWhere (void)
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// Code qui peut accéder à CURRENT_USER.get()
doSomething();
});
// Avec callWhere (retourne une valeur)
String result = ScopedValue.callWhere(CURRENT_USER, user, () -> {
return processAndReturn();
});
// Avec getWhere (accès direct à la valeur)
String name = ScopedValue.where(CURRENT_USER, user)
.call(() -> CURRENT_USER.get().getName());
Lecture de la valeur
// Lecture simple (lève NoSuchElementException si hors scope)
User user = CURRENT_USER.get();
// Vérification de la présence
if (CURRENT_USER.isBound()) {
User user = CURRENT_USER.get();
}
// Avec valeur par défaut
User user = CURRENT_USER.orElse(User.ANONYMOUS);
// Avec supplier
User user = CURRENT_USER.orElseGet(() -> loadDefaultUser());
Scopes imbriqués
Les Scoped Values supportent les scopes imbriqués. Un scope interne peut "masquer" la valeur d'un scope externe.
private static final ScopedValue<String> CONTEXT = ScopedValue.newInstance();
public void demonstrerImbrication() {
ScopedValue.runWhere(CONTEXT, "externe", () -> {
System.out.println(CONTEXT.get()); // "externe"
ScopedValue.runWhere(CONTEXT, "interne", () -> {
System.out.println(CONTEXT.get()); // "interne"
});
System.out.println(CONTEXT.get()); // "externe" (restauré)
});
}
Rebinding dans le scope
// Utiliser where().run() pour combiner plusieurs valeurs
ScopedValue.where(USER, user)
.where(TRANSACTION, tx)
.where(REQUEST_ID, requestId)
.run(() -> {
processWithContext();
});
Utilisation avec les Virtual Threads
Les Scoped Values sont optimisées pour les Virtual Threads introduits dans Java 21.
Contrairement à InheritableThreadLocal qui copie les valeurs, les Scoped Values
sont automatiquement héritées sans copie.
private static final ScopedValue<User> USER = ScopedValue.newInstance();
public void handleRequestWithVirtualThreads(User user) {
ScopedValue.runWhere(USER, user, () -> {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Les Virtual Threads créés ici héritent automatiquement de USER
executor.submit(() -> {
// USER.get() fonctionne ici sans copie !
processInVirtualThread();
});
executor.submit(() -> {
// Et ici aussi
anotherTask();
});
}
});
}
Cas d'utilisation pratiques
1. Contexte de requête web
public class RequestContext {
public static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public static final ScopedValue<Locale> LOCALE = ScopedValue.newInstance();
}
@WebFilter("/*")
public class ContextFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
HttpServletRequest httpReq = (HttpServletRequest) req;
String requestId = UUID.randomUUID().toString();
User user = authenticateUser(httpReq);
Locale locale = determineLocale(httpReq);
ScopedValue.where(RequestContext.REQUEST_ID, requestId)
.where(RequestContext.CURRENT_USER, user)
.where(RequestContext.LOCALE, locale)
.run(() -> {
chain.doFilter(req, resp);
});
}
}
// Dans n'importe quel service
public class OrderService {
public Order createOrder(List<Item> items) {
User user = RequestContext.CURRENT_USER.get();
String requestId = RequestContext.REQUEST_ID.get();
logger.info("[{}] Création commande pour {}", requestId, user.getId());
// ...
}
}
2. Contexte de transaction
public class TransactionContext {
public static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();
public static final ScopedValue<String> TRANSACTION_ID = ScopedValue.newInstance();
public static <T> T executeInTransaction(Supplier<T> operation) {
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
String txId = generateTxId();
try {
T result = ScopedValue.where(CONNECTION, conn)
.where(TRANSACTION_ID, txId)
.call(operation::get);
conn.commit();
return result;
} catch (Exception e) {
conn.rollback();
throw e;
}
}
}
}
// Utilisation
public void transferMoney(Account from, Account to, BigDecimal amount) {
TransactionContext.executeInTransaction(() -> {
Connection conn = TransactionContext.CONNECTION.get();
// Utiliser la connexion pour les opérations
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
return null;
});
}
3. Contexte de sécurité
public class SecurityContext {
public static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
public static final ScopedValue<Set<String>> ROLES = ScopedValue.newInstance();
public static void requireRole(String role) {
Set<String> roles = ROLES.orElse(Set.of());
if (!roles.contains(role)) {
throw new AccessDeniedException("Rôle requis: " + role);
}
}
public static <T> T runAs(Principal principal, Set<String> roles, Supplier<T> action) {
return ScopedValue.where(PRINCIPAL, principal)
.where(ROLES, roles)
.call(action::get);
}
}
4. Contexte de logging
public class LogContext {
public static final ScopedValue<Map<String, String>> MDC = ScopedValue.newInstance();
public static void withContext(Map<String, String> context, Runnable action) {
Map<String, String> merged = new HashMap<>(MDC.orElse(Map.of()));
merged.putAll(context);
ScopedValue.runWhere(MDC, Map.copyOf(merged), action);
}
}
// Utilisation
LogContext.withContext(Map.of("orderId", "12345"), () -> {
// Tous les logs dans ce scope auront orderId
logger.info("Traitement commande"); // [orderId=12345] Traitement commande
});
Comparaison : ThreadLocal vs ScopedValue
| Aspect | ThreadLocal | ScopedValue |
|---|---|---|
| Mutabilité | Mutable (set/get) | Immuable dans le scope |
| Durée de vie | Durée du thread | Durée du scope |
| Nettoyage | Manuel (remove()) | Automatique |
| Héritage | Copie coûteuse | Sans copie |
| Virtual Threads | Problématique | Optimisé |
Migration de ThreadLocal vers ScopedValue
// Avant - ThreadLocal
public class AvantMigration {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void setCurrentUser(User user) {
currentUser.set(user);
}
public User getCurrentUser() {
return currentUser.get();
}
public void clearCurrentUser() {
currentUser.remove();
}
public void handleRequest(User user) {
setCurrentUser(user);
try {
processRequest();
} finally {
clearCurrentUser();
}
}
}
// Après - ScopedValue
public class ApresMigration {
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public static User getCurrentUser() {
return CURRENT_USER.get();
}
public void handleRequest(User user) {
ScopedValue.runWhere(CURRENT_USER, user, this::processRequest);
}
}
Bonnes pratiques
- Déclarez les ScopedValue en static final : comme des constantes
- Utilisez des noms en SCREAMING_CASE : convention pour les constantes
- Préférez les types immuables : évitez de stocker des objets mutables
- Gardez les scopes courts : uniquement pour la durée nécessaire
- Documentez l'usage : indiquez quand une ScopedValue doit être bound
Conclusion
Les Scoped Values de Java 25 (JEP 506) représentent une évolution majeure pour le partage
de contexte en Java. Cette API moderne, immuable et performante remplace avantageusement
ThreadLocal, particulièrement dans le contexte des Virtual Threads.
Pour les nouvelles applications Java 25, préférez systématiquement les Scoped Values. Pour les applications existantes, planifiez une migration progressive lors des refactorisations.