Gymterview
middle

Что такое 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 выше точки чтения.