Что такое Context в Project Reactor?
Context — неизменяемая (immutable) структура данных в Project Reactor, привязанная к конкретной подписке (Subscription). Замена ThreadLocal для реактивных цепочек, где один запрос может исполняться в разных потоках.
Аналогия из жизни: Context — это как бейджик участника конференции. Не важно, в какой зал (поток) вы перейдёте — бейджик (контекст) остаётся при вас и идентифицирует вас в любом месте.
Проблема
В реактивном коде ThreadLocal не работает, потому что цепочка операторов может переключаться между потоками через publishOn/subscribeOn. Context решает эту проблему.
Запись и чтение контекста
Пример
Mono<String> mono = Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
return Mono.just("User: " + userId);
})
.contextWrite(Context.of("userId", "12345"));
// Результат: "User: 12345"
Практический пример — передача correlation ID
@Component
public class CorrelationWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String correlationId = exchange.getRequest().getHeaders()
.getFirst("X-Correlation-ID");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
String finalCorrelationId = correlationId;
return chain.filter(exchange)
.contextWrite(Context.of("correlationId", finalCorrelationId));
}
}
// Использование в сервисе
@Service
public class OrderService {
public Mono<Order> createOrder(OrderRequest request) {
return Mono.deferContextual(ctx -> {
String correlationId = ctx.getOrDefault("correlationId", "unknown");
log.info("[{}] Создание заказа", correlationId);
return orderRepository.save(new Order(request));
});
}
}
Важные особенности
- Context immutable — каждый
contextWriteсоздаёт новый Context - Context распространяется снизу вверх (от subscribe к источнику) —
contextWriteдолжен быть ниже точки чтения deferContextual— для чтения контекста при создании элементовtransformDeferredContextual— для трансформации в середине цепочки
Пример
// Ближний к подписке contextWrite перезаписывает дальний
Mono<String> mono = Mono.deferContextual(ctx ->
Mono.just(ctx.get("key")))
.contextWrite(ctx -> ctx.put("key", "value2")) // ближе к подписке
.contextWrite(ctx -> ctx.put("key", "value1")); // дальше от подписки
// Результат: "value2"
Частые ошибки
- Использовать ThreadLocal в реактивном коде — значение потеряется при смене потока
- Помещать
contextWriteвыше точки чтения — Context распространяется снизу вверх - Хранить мутабельные объекты в Context — лучше использовать immutable типы
- Злоупотреблять Context — это не замена параметров метода; Context для cross-cutting concerns (tracing, auth, MDC)
Как используется в 2026
- Reactor Context — стандарт для передачи correlation ID, trace ID, информации об аутентификации
- Micrometer + Reactor автоматически пробрасывает observation context через Reactor Context
- Spring Security Reactive использует Context для хранения SecurityContext
- С Virtual Threads потребность снижается (можно использовать ThreadLocal через ScopedValue)
На собеседовании: ключевое — объяснить, зачем нужен Context (замена ThreadLocal) и что он распространяется снизу вверх. Частая ошибка — не знать направление распространения Context и разместить contextWrite выше точки чтения.