Оглавление
- Введение
- Проблематика: ограничения блокирующей модели
- Варианты решения проблемы
- Как Spring WebFlux решает эти проблемы
- Основные концепции реактивного стека (WebFlux и Reactor)
- Примеры кода: первые шаги с WebFlux
- WebFlux vs виртуальные потоки: заменят ли Loom/Virtual Threads реактивный стек?
- Рекомендации: когда использовать WebFlux, а когда достаточно Spring MVC
- Заключение
Введение
Когда производительность и масштабируемость системы становятся критически важными, традиционные блокирующие веб-фреймворки могут оказаться недостаточными. Именно для таких задач был создан Spring WebFlux – асинхронный неблокирующий веб-фреймворк, основанный на реактивной парадигме программирования. Spring WebFlux появился в Spring 5 как реактивная альтернатива Spring MVC для разработки веб-сервисов. В отличие от классического Spring MVC, WebFlux не требует Servlet API, полностью асинхронен, не блокирует потоки и реализует спецификацию Reactive Streams посредством проекта Reactor. Проще говоря, WebFlux позволяет обрабатывать множество одновременных запросов с минимумом потоков, эффективно используя ресурсы и сохраняя низкую задержку.
В этой статье мы подробно рассмотрим проблемы традиционного подхода, узнаем какие решения были придуманы для повышения масштабируемости, поймем как WebFlux решает эти проблемы, разберем базовые концепции реактивного программирования, приведем примеры кода для начинающих, сравним WebFlux с новыми возможностями виртуальных потоков Java и обсудим когда имеет смысл использовать WebFlux, а когда лучше обойтись стандартным Spring MVC.
Проблематика: ограничения блокирующей модели
Традиционные веб-приложения на Spring MVC используют модель “один запрос – один поток”. Каждому входящему HTTP-запросу сопоставляется поток из пула (например, потоки Tomcat), и этот поток выполняет обработку в контроллере и остальные действия, пока не сформирует ответ. Проблема в том, что при выполнении I/O-операций (обращение к базе данных, вызов внешнего сервиса, чтение файла и т.д.) поток простаивает, будучи заблокированным до получения результата. На это время CPU ничего не делает для данного запроса, а поток фактически теряется впустую. Такой подход прост в реализации, но под высокой нагрузкой быстро достигает предела: размер пула потоков не бесконечен (стандартно – 200 потоков), и если все потоки заняты ожиданием, новые запросы вынуждены стоять в очереди, увеличивая задержки. Ситуация усугубляется, если каждый запрос выполняет несколько последовательных внешних вызовов – общее время блокировки растет, блокируя поток на каждом этапе.
Проиллюстрируем это на диаграмме. В традиционном блокирующем стеке (Spring MVC + Tomcat) каждый запрос обслуживается отдельным потоком, который блокируется на время выполнения внешних операций:

Рис. 1: обработка запроса в блокирующем стеке
На диаграмме видно, что оба потока сервера заблокированы во время ожидания ответа от базы данных. Если одновременных запросов станет больше, чем потоков в пуле, новые запросы просто не получат поток для обработки, пока предыдущие не освободятся. Кроме того, каждый поток Java занимает память (стек, контекст) и переключение контекста между сотнями потоков также съедает ресурсы. Таким образом, модель с блокирующими потоками плохо масштабируется под высокую конкурентную нагрузку: при большом числе одновременных соединений сервер либо начнет отклонять лишние запросы, либо задержки резко вырастут.
Итоговые проблемы блокирующей модели:
- Простоящие потоки: значительная часть времени потоки бездействуют, ожидая I/O, не загружая CPU.
- Высокое потребление ресурсов: для обслуживания тысяч запросов нужны тысячи потоков, что расходует память и ведет к частому переключению контекста.
- Ограниченная масштабируемость: пул потоков конечен, при нагрузке, превышающей размер пула, приложение перестает справляться (появляются таймауты, ошибки нехватки потоков и т.п.).
- Задержки под нагрузкой: рост числа одновременно активных пользователей увеличивает среднее время ответа, так как запросы выстраиваются в очередь на обслуживание.
Эти ограничения стали особенно болезненными в эпоху микросервисов и облачных систем, где один сервис может одновременно обслуживать множество клиентов и делать несколько сетевых вызовов для каждого запроса. Необходимо было найти способ эффективнее использовать потоки и не тратить их впустую при ожидании I/O.
Варианты решения проблемы
За годы развития серверных технологий было предложено несколько подходов для решения описанных проблем с масштабируемостью и блокирующими потоками:
- Более крупные ресурсы / вертикальное масштабирование – самый тривиальный путь: добавить больше потоков, увеличить пул, выделить больше CPU и памяти, чтобы выдерживать больше одновременных запросов. Этот подход работает до поры до времени, но имеет предел – нагрузка может расти быстрее, чем ресурсы, а эффективность падает (1000 потоков дадут лишь небольшую пользу из-за накладных расходов). Это тупиковый путь с точки зрения оптимизации, хотя в небольших системах часто просто увеличивают пул потоков или число серверов.
- Асинхронное неблокирующее программирование (callbacks/futures) – более элегантное решение: не ждать результата в потоке, а продолжать выполнять другие задачи, получив результат позже через колбэк (обратный вызов). В среде Java это долгое время было сложно реализовать – приходилось вручную работать с Future, слушателями или использовать низкоуровневый NIO. Тем не менее, подход событийного цикла (event loop), популярный, например, в Node.js, позволяет одному потоку обслуживать многие соединения, регистрируя колбэки на события (данные получены, запрос завершен и т.д.). Аналогично, в Java существуют Netty, Vert.x и другие фреймворки, предоставляющие API для неблокирующей обработки I/O на основе событий. Главное преимущество – потоки не простаивают: когда одна операция начата, поток может переключиться на обработку другого соединения, а когда приходят данные для первого – получить уведомление и продолжить работу по нему. Недостаток классических колбэков – сложность кода (callback hell), разорванная логика, трудности с обработкой ошибок и поддержкой последовательности операций.
- Реактивное программирование и Reactive Streams – это эволюция асинхронного подхода, направленная на структурирование и упрощение работы с асинхронными потоками данных. В 2010-х появились библиотеки вроде RxJava, Reactor, Akka Streams, которые предоставляют декларативный API для описания последовательности асинхронных операций (через потоки событий, Observable, Flux/Mono и т.п.). Появилась и спецификация Reactive Streams (2015) для совместимости таких библиотек. Идея в том, чтобы программировать в функциональном стиле, реагируя на поступление данных, а не блокируя потоки в ожидании. Реактивный код позволяет писать цепочки преобразований (операторы map, flatMap и др.), избегая вложенных колбэков, при этом под капотом все равно используется неблокирующий I/O и цикл событий. Spring WebFlux как раз и построен на этом принципе: он использует библиотеку Project Reactor, реализующую Reactive Streams, чтобы достичь высокой масштабируемости без блокировок.
- Виртуальные потоки (Project Loom) – новейший подход, появившийся в Java 19-21. Виртуальные потоки – это легковесные потоки, реализованные на уровне JVM (их еще называют зелеными потоками). Они позволяют запускать десятки тысяч потоков, каждый из которых не привязан жестко к системному потоку. Блокировка в виртуальном потоке не удерживает ОС-ресурс: когда виртуальный поток блокируется (на I/O), он отцепляется от реального потока-носителя, который может выполнять другие задачи. Тем самым достигается эффект, близкий к реактивному подходу, – многопоточность при сохранении привычной императивной модели программирования. Virtual Threads существенно снижают потребность в сложном асинхронном коде: можно писать обычный последовательный код, а JVM под капотом будет эффективно распараллеливать его на небольшом пуле реальных потоков. Далее в статье мы подробнее сравним WebFlux и виртуальные потоки.
Помимо перечисленных, есть и другие смежные техники: например, разделение нагрузки между микросервисами (горизонтальное масштабирование), использование очередей сообщений (Kafka, RabbitMQ) для разгрузки синхронных взаимодействий, и т.д. Однако в контексте данной статьи нас интересуют прежде всего технологические решения на уровне сервера и фреймворка, позволяющие обходиться без блокировки потоков. Spring WebFlux представляет именно такой подход – использование реактивного стека. Рассмотрим, как он устраняет ограничения блокирующей модели.
Как Spring WebFlux решает эти проблемы
Spring WebFlux реализует неблокирующую архитектуру на базе реактивного программирования, что позволяет обслуживать большое число запросов, не удерживая поток на каждый из них. Главный принцип – не блокировать поток ожиданием I/O, а работать через реактивные стримы с обратным вызовом (callback) по готовности данных. В WebFlux запрос обрабатывается следующим образом (упрощенно):
- Когда запрос поступает, он принимается сервером Netty (по умолчанию) и передается в обработчик (контроллер) без выделения отдельного потока на весь запрос. Netty использует небольшой пул потоков – Event Loop (обычно по числу ядер) для обработки всех соединений.
- Контроллер (написанный разработчиком) не возвращает сразу готовые данные, как в MVC, а возвращает реактивный тип: Mono или Flux (моно-поток или мульти-поток данных). Это своего рода обещание (promise) предоставить результат позже. Например, Mono<User> означает “результат будет User, когда он появится”.
- Внутри контроллера, если нужно обратиться к базе или другому сервису, используются реактивные драйверы/клиенты. Например, вместо JdbcTemplate – реактивный репозиторий R2DBC, вместо RestTemplate – WebClient. Эти вызовы сразу возвращают Mono/Flux без ожидания результата. Контроллер может комбинировать эти потоки (например, выполнить несколько запросов параллельно, объединить результаты, преобразовать данные) через операторы Reactor, и в конце возвращает итоговый Mono/Flux.
- После выхода из контроллера сам фреймворк WebFlux подписывается (subscribe) на полученный Publisher. Фреймворк не создает новый поток для ожидания – вместо этого Netty продолжает слушать события. Когда, например, придет ответ из базы (асинхронно, через NIO), соответствующий поток Netty пробуждается, и WebFlux продолжает выполнение цепочки операторов Reactor для этого запроса.
- Как только в Mono/Flux появляется готовый результат (или серия результатов для Flux), WebFlux отправляет их в HTTP-ответ и завершает обработку.
Ключевое отличие: потоки не простаивают впустую. Пока запрос A ждет ответ от БД, тот же поток Event Loop может заняться запросом B или C. Ниже представлена последовательность обработки в WebFlux (на базе Netty), где один поток Event Loop чередует работу с двумя запросами по мере готовности данных:

Рис. 2: обработка запроса в неблокирующем стеке
Здесь один поток справляется с двумя запросами, потому что операции ввода-вывода не блокируют его – они выполняются в фоновом режиме, а поток получает уведомление (callback) по завершении. Идеально, если ни один из этапов обработки не блокирует поток – тогда несколько потоков Event Loop способны обслуживать тысячи соединений. Практически это означает, что все части стека должны быть реактивными: веб-сервер, контроллеры, клиент к БД, вызовы к внешним сервисам – все должно поддерживать асинхронный неблокирующий режим. В противном случае, одна блокирующая часть сведет на нет эффект WebFlux: “в реактивной системе даже один блокирующий вызов может все сломать”. Разработчикам приходится помнить об этом: например, при работе с WebFlux недопустимо вызывать методы типа .block() или использовать старые JDBC-драйверы прямо в контроллере – этим вы заблокируете Event Loop поток и потеряете преимущество. Вместо JDBC применяется R2DBC, вместо Thread.sleep() – неблокирующие задержки (Mono.delay), и т.п.
Малое число потоков. Поскольку WebFlux не выделяет по потоку на запрос, ему достаточно всего несколько потоков. Netty обычно заводит ~4-8 потоков (зависит от ядер CPU) для обработки всех сетевых событий. Даже с тысячами одновременных соединений эти потоки успевают их обслуживать, переключаясь между задачами. Таким образом, уменьшается потребление памяти и нагрузки на планировщик ОС. В среднем, ри нагрузке 300+ одновременных пользователей WebFlux на Netty способен обслуживать в ~2 раза больше запросов, чем Tomcat с потоками, прежде чем упереться в пределы сервера. В различных тестах преимущество может отличаться, но тренд очевиден: неблокирующая модель лучше масштабируется под большое число I/O-запросов.
Backpressure (обратное давление). Одно из ключевых отличий реактивного подхода – наличие механизма регулировки скорости потока данных. В синхронном коде “блокировка” сама по себе выступает примитивным backpressure – отправитель вынужден ждать, пока получатель освободится. В асинхронном коде нужно явно следить, чтобы быстрый продюсер не перегрузил более медленного консюмера. Спецификация Reactive Streams вводит протокол, по которому потребитель (Subscriber) запрашивает у источника определенное количество элементов (request(n)), и источник не может послать больше, пока не получит новый запрос. Это и есть неблокирующее управление нагрузкой. Если же источник не может замедлиться (например, бесконечный генератор событий), то он должен либо буферизовать лишние данные, либо отбросить, либо сигнализировать об ошибке – но не заливать потребителя сверх запрошенного. WebFlux поддерживает backpressure во всех своих компонентах, так как основан на Reactor, полностью реализующем Reactive Streams. Благодаря этому при огромном числе событий система ведет себя корректно: лишние данные накапливаются в буфере до определенного лимита или передача замедляется. Пример: если клиент потребляет данные медленно (например, медленный websocket-потребитель), то Flux на сервере не будет бесконтрольно накапливать в памяти гигабайты непрочитанных данных – он отправит ровно столько, сколько клиент успевает принять, либо прекратит поток по ошибке, избегая OutOfMemory.
Reactor и основные типы. Под капотом Spring WebFlux использует библиотеку Project Reactor – это реактивная библиотека от команды Spring, аналогичная RxJava. Reactor предоставляет два основных типа:
- Mono (эмитирует 0 или 1 элемент)
- Flux (эмитирует 0..N элементов).
Вся работа с данными в WebFlux строится вокруг этих типов. Например, запрос на получение одного объекта (пользователя по ID) будет возвращать Mono<User>, а запрос списка пользователей – Flux<User> (т.е. поток из нескольких элементов). Mono/Flux сами по себе ничего не делают, пока на них не подпишутся – они ленивые (lazy). Фреймворк WebFlux подписывается на них, когда нужно отправить ответ, – тогда цепочка начинает исполняться. Это означает, что можно безопасно описывать последовательность операций (например, repo.findById(id).map(…).flatMap(…)) – ничего из этого не выполнится до момента subscribe.
Важно: выполнение реактивного кода происходит не мгновенно, а когда данные реально доступны, поэтому методы контроллера быстро возвращают Mono/Flux, не задерживая поток.
Чтобы познакомиться ближе с философией Reactor, рассмотрим простой пример на Mono. Создадим Mono, содержащий строку, и применим к ней оператор map для преобразования, выведя результат:
Mono<String> mono = Mono.just("hello")
.map(s -> s.toUpperCase()); // преобразует строку к верхнему регистру
mono.subscribe(result -> System.out.println("Результат: " + result));
Этот код создает Mono<String> с значением “HELLO” (при подписке). Оператор map не выполняется сразу – лишь при вызове subscribe начинается обработка, и в консоль выводится Результат: HELLO. Так работает декларативное построение реактивной цепочки: вы описываете, что сделать с данными (преобразовать строку), а выполнение отложено до фактического запроса результата. Mono и Flux имеют богатый набор операторов (более 200 в Reactor) для трансформации, фильтрации, объединения потоков, обработки ошибок и т.д. Это позволяет выразить сложную асинхронную логику в понятной потоковой форме, похожей на работу с Java Stream API, но для асинхронных последовательностей данных.
Аннотации vs функциональный стиль. Spring WebFlux поддерживает две программные модели для описания контроллеров: знакомый нам способ – через аннотации (как в Spring MVC, с @RestController и аннотациями @GetMapping и т.п.) и функциональный (обработчики и маршруты, задаваемые через функциональный API). Функциональный стиль позволяет описывать маршрутизацию запросов и обработчики как бины, например с использованием RouterFunction и HandlerFunction. В рамках данной статьи мы будем в основном использовать аннотации, так как он проще для понимания новичкам (он практически такой же, как в MVC, за исключением возвращаемых типов). Но имейте в виду: под капотом оба стиля работают одинаково эффективно и оба полностью реактивны.
Итого, WebFlux решает проблемы масштабируемости за счет неблокирующего ввода-вывода, использования событийной модели (event loop), и реактивного протокола с Mono/Flux, который не позволяет “утонуть” в потоке данных. Вместо того чтобы плодить потоки под каждое соединение, WebFlux заставляет данные ждать потоков, а не потоки ждать данные. Это коренной сдвиг в архитектуре веб-приложений на Java. Разумеется, такой подход накладывает и ограничения – от разработчика требуются новый стиль кодирования и тщательное соблюдение реактивности на всех уровнях. Далее мы рассмотрим основные концепции реактивного стека подробнее, а затем перейдем к практике.
Основные концепции реактивного стека (WebFlux и Reactor)
Прежде чем писать код с WebFlux, важно понять несколько базовых понятий реактивного программирования и инструменты, которые предоставляет платформа:
- Спецификация Reactive Streams. Это стандарт для взаимодействия асинхронных компонентов с поддержкой backpressure (описанный выше механизм обратного давления). Вкратце, он определяет четыре роли: Publisher (источник данных), Subscriber (получатель данных), Subscription (связь между ними) и Processor (опциональный промежуточный обработчик). Publisher при подписке передает подписчику Subscription, через который тот запрашивает элементы (request(n)). Таким образом, потребитель контролирует скорость. Spring WebFlux строится на этой спецификации: Flux и Mono реализуют интерфейс Publisher, а Spring внутри выступает Subscriber’ом, когда нужно отправить результаты HTTP-клиенту. Другие библиотеки (RxJava, Akka Streams, etc.) могут интегрироваться благодаря общему стандарту.
- Project Reactor, Mono и Flux. Reactor – реализация Reactive Streams от Spring. Два основных класса Reactor: Mono (для 0..1 элемента) и Flux (для 0..N элементов). Эти типы представляют асинхронный поток данных (может прийти сейчас, может позже, может не прийти вообще). Mono похоже на CompletionStage/Future в том плане, что содержит один результат или ошибку. Flux похож на поток Iterable, который поступает постепенно. Оба типа поддерживают множество операторов для построения конвейеров обработки:
- map – трансформация каждого элемента;
- flatMap – асинхронное отображение (каждый элемент может породить новый Mono/Flux, которые затем сливаются);
- filter, take, reduce и др. – как в стримах Java, только применяются реактивно;
- Специфичные операторы: delayElements (задержка), timeout (таймаут ожидания), retry (повтор при ошибке), onErrorResume (восстановление после ошибки) и т.д.
Пример цепочки: userRepo.findById(id).flatMap(user -> orderRepo.findLastByUser(user.id)) .map(order -> new UserWithOrder(user, order)). Здесь findById возвращает Mono<User>, при его приходе выполняется flatMap для получения последнего заказа пользователя (тоже Mono), затем обе Mono объединяются – на выходе получаем Mono с объединенными данными пользователя и его заказа. Такая композиция заменяет вложенные запросы и if/else логіку, делая код декларативным.
- Холодные vs горячие потоки. По умолчанию Mono/Flux в Reactor – холодные (cold) publishers. Это значит, что они начинают испускать данные только в момент подписки, и для каждого нового подписчика выполнение начинается заново. Например, Flux.fromIterable(list) при каждом subscribe будет итерироваться по списку с начала. Аналогично Mono.fromCallable(() -> compute()) вызовет compute() для каждого подписчика. Это удобно: мы можем повторно запускать цепочку обработки для разных подписчиков. Горячие (hot) источники – это напротив, источники событий, которые генерируются независимо от подписок (например, Flux.interval или поток сообщений из внешней системы). Горячий поток нельзя “начать сначала”, новый подписчик получит только текущие и будущие элементы. В WebFlux подавляющее большинство сценариев – холодные Mono/Flux, так как на каждый HTTP-запрос создается свой реактивный поток данных. Но понимание этой разницы важно при использовании таких вещей, как соединения WebSocket или Server-Sent Events, где поток данных постоянный и шарится между подписчиками.
- Шедулеры и потоки исполнения. Reactor управляет выполнением операторов с помощью Schedulers – абстракций над пулами потоков. По умолчанию, если явно не указано иное, Mono/Flux выполняются на том же потоке, где подписаны. В WebFlux подписка происходит на Netty-IO потоке, поэтому весь реактивный конвейер по умолчанию работает на Netty event loop. Это значит, что внутри операторов нельзя блокировать (нельзя делать Thread.sleep, вызывать синхронный JDBC и т.п.), иначе вы заблокируете event loop поток! Если необходимо выполнить блокирующую операцию внутри реактивной цепочки (например, вызов legacy-кода или интенсивный CPU-расчет), Reactor предлагает специальный планировщик boundedElastic – отдельный пул потоков, на который можно переключиться оператором .publishOn(Schedulers.boundedElastic()). Этот пул состоит из “эластичных” потоков, которые могут расти по необходимости, но их использование в реактивном контексте – крайний случай. В идеале, все что можно, следует использовать неблокирующее. Появление виртуальных потоков в будущем может упростить эту задачу (см. раздел 6), но пока правило такое: не блокируй event loop!. Впрочем, если соблюдать это правило, то вам редко придется явно управлять Schedulers – WebFlux делает все автоматически.
- WebClient и другие реактивные API. Spring WebFlux включает в себя не только серверный фреймворк, но и набор инструментов для реактивного стека. Например, WebClient – это реактивный HTTP-клиент, альтернатива RestTemplate. Он позволяет выполнять HTTP-запросы неблокирующим способом и возвращает Mono/Flux. Пример использования WebClient:
WebClient client = WebClient.create("https://api.example.com");
Mono<Weather> weatherMono = client.get()
.uri("/weather?city={city}", cityName)
.retrieve()
.bodyToMono(Weather.class);
Здесь weatherMono – обещание получить объект Weather с удаленного сервиса. Мы можем прицепить weatherMono к цепочке обработки или вернуть из контроллера прямо – WebFlux сам подпишется и подождет ответ, не занимая поток. Есть также WebTestClient для тестирования WebFlux-контроллеров, реактивные драйверы баз данных (Spring Data R2DBC), реактивные стримы для MongoDB, Redis, и даже реактивная поддержка WebSocket и SSE. Все это расширяет возможности создания полноценных реактивных систем на Spring.
В совокупности, эти концепции позволяют строить приложения, где поток данных – первичен, а потоки ОС – вторичны. Код выглядит иначе (больше функциональных вызовов, Mono/Flux вместо привычных объектов), но такой стиль открывает двери к высокой масштабируемости. Далее перейдем от теории к практике – создадим небольшие примеры WebFlux-приложений.
Примеры кода: первые шаги с WebFlux
Начнем с того, как подключить WebFlux в проект. Если вы используете Spring Boot, достаточно добавить зависимость стартер-фреймворка WebFlux. В Maven это выглядит так:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring-boot.version}</version>
</dependency>
Этот стартер подтянет все необходимые компоненты: Spring WebFlux, сервер Netty, Reactor Core и Reactor Netty и др.. Обратите внимание: он заменяет обычный spring-boot-starter-web (который тянет Spring MVC с Tomcat). То есть, нельзя одновременно использовать MVC и WebFlux в одном приложении – нужно выбрать что-то одно при конфигурации стартера.
Простой REST-контроллер (Mono и Flux)
Создадим простой контроллер, который возвращает данные о пользователях. Предположим, у нас есть сервис UserService с методами findById(String) и findAll(), которые реализованы реактивно (например, через R2DBC или в памяти, но возвращают Mono/Flux). Наш контроллер будет выглядеть так:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable String id) {
// Возвращаем Mono<User>, который асинхронно выдаст пользователя
return userService.findById(id);
}
@GetMapping
public Flux<User> getAllUsers() {
// Возвращаем Flux<User> - поток пользователей (например, из БД)
return userService.findAll();
}
}
Как видите, отличия от Spring MVC минимальны: те же аннотации контроллера, те же маршруты. Разница в типе возвращаемого значения – вместо User или List<User> мы возвращаем Mono<User> и Flux<User>. Spring WebFlux распознает эти реактивные типы и знает, как их обработать. Когда придет HTTP-запрос GET /users/123, фреймворк вызовет getUserById, получит Mono, подпишется на него и отправит HTTP-ответ, когда Mono передает значение пользователя (или пустой результат/ошибку). Аналогично для списка: Flux пользователей будет конвертирован, например, в JSON-стрим (с возможностью отправлять элементы по мере готовности, если включен JSON streaming или SSE). Обычно Spring буферизует Flux до окончания и отправляет целиком как массив JSON, но при желании можно стримить частями.
Для справки: согласно документации, одиночный результат мы оборачиваем в Mono, потому что возвращаем максимум один элемент, а коллекцию – в Flux, потому что это 0..N элементов. Если метод может вернуть отсутствие результата, в Mono можно передавать Mono.empty() (это эквивалент null в реактивном мире). При ошибке – вернуть Mono.error(exception). Эти детали управления потоком данных заменяют исключения и null возвраты в MVC контроллерах.
Вызов внешних сервисов (WebClient)
Часто в бэкенде нужно вызывать другие REST-сервисы. В мире WebFlux для этого предназначен неблокирующий клиент WebClient. Рассмотрим пример: наш SmsService должен вызывать внешний API отправки SMS. С WebClient это делается реактивно:
@Service
public class SmsService {
private final WebClient smsClient;
public SmsService(WebClient.Builder webClientBuilder) {
// предположим, baseUrl задается в настройках
this.smsClient = webClientBuilder.baseUrl("https://sms.example.com").build();
}
public Mono<ResultDto> sendSms(String phone, String text) {
MessageDto message = new MessageDto(phone, text);
// Отправляем POST-запрос к внешнему сервису
return smsClient.post()
.uri("/send")
.bodyValue(message)
.retrieve()
.bodyToMono(ResultDto.class); // получаем ответ как Mono<ResultDto>
}
}
Здесь smsClient.post().uri(“/send”)… формирует запрос. Метод retrieve() выполняет запрос и готов вернуть ответ, но не блокирует – вместо этого bodyToMono(ResultDto.class) возвращает Mono<ResultDto>, которое будет заполнено, когда придет HTTP-ответ. Метод sendSms нашего сервиса сразу возвращает этот Mono. Мы можем использовать его в контроллере примерно так:
@GetMapping("/sendSms")
public Mono<ResultDto> sendSmsEndpoint(@RequestParam String phone, @RequestParam String text) {
return smsService.sendSms(phone, text);
}
Контроллер вернет Mono<ResultDto> клиенту. WebFlux подпишется и, когда Mono завершится (либо успехом, либо ошибкой), вернет HTTP-ответ. Весь этот процесс опять же неблокирующий: ни на одном этапе поток не ждет синхронно ответов.
Стоит подчеркнуть: WebClient сам по себе не требует WebFlux на сервере – его можно использовать и в приложении с Spring MVC для вызова внешних сервисов реактивно. Но тогда, если вы получаете Mono и хотите дальше работать синхронно, придется вызвать .block(), что нивелирует пользу. Поэтому WebClient идеально вписывается именно в реактивные цепочки WebFlux, где вы можете вернуть Mono как есть или комбинировать его с другими потоками.
Параллельные запросы и объединение результатов
Покажем силу реактивной модели на примере: допустим, нужно одновременно вызвать несколько независимых сервисов и собрать результаты. В императивном коде вы бы вызывали их последовательно или использовали CompletableFuture.allOf. В WebFlux легко сделать это с помощью Flux и операторов объединения.
Представьте, у нас есть три внешних сервиса, к которым надо обратиться параллельно (например, для агрегирования данных). Мы можем создать список URL и воспользоваться Flux.fromIterable + flatMap:
List<String> urls = List.of("/service1/data", "/service2/info", "/service3/stats");
Flux<String> responsesFlux = Flux.fromIterable(urls)
.flatMap(url -> webClient.get().uri(url).retrieve().bodyToMono(String.class));
Здесь на каждый URL выполняется GET-запрос через WebClient, получая Mono<String>, и flatMap обеспечивает их конкурентное выполнение. Итоговый responsesFlux будет передавать результаты по мере готовности каждого запроса. Причем flatMap сам позаботится, чтобы все запросы отправились параллельно (в рамках доступных ресурсов). Если первый сервис ответит за 5 секунд, второй за 10, третий мгновенно – суммарно все завершится примерно за 10 секунд, а не 15, как было бы при последовательных вызовах. Мы можем затем, например, собрать эти результаты в список: responsesFlux.collectList() вернет Mono<List<String>> со всеми ответами.
Другой случай – вам нужно скомбинировать результаты нескольких источников, зависящих друг от друга. Например, сначала получить пользователя, потом на основе его данных – список заказов, потом объединить. Реактивно это делается вложенными flatMap. Вот шаблон:
Mono<UserProfile> profile = userService.findById(userId)
.flatMap(user -> orderService.getLatestOrder(user.getId())
.map(order -> new UserProfile(user, order))
);
Алгоритм: ждем user из первого Mono, потом запускаем второй запрос getLatestOrder, получаем order и сразу мапим в объект UserProfile, содержащий и пользователя, и его последний заказ. Возвращаем Mono<UserProfile>. Благодаря цепочке flatMap+map, код выглядит последовательным по логике, но выполняется асинхронно и неблокирующе. Если getLatestOrder может работать параллельно с другими операциями, можно использовать Mono.zip. Например, чтобы параллельно загрузить данные пользователя и его настройки профиля из разных сервисов:
Mono<User> userMono = userService.findById(id);
Mono<Preferences> prefMono = preferencesService.getByUserId(id);
Mono<UserWithPrefs> combined = Mono.zip(userMono, prefMono, (user, prefs) -> new UserWithPrefs(user, prefs));
Здесь оба запроса выполняются одновременно, и когда оба Mono завершатся, в функцию-комбайнер придут результаты.
Эти приемы – сердце реактивного программирования: думать в терминах композиции асинхронных задач, а не потоков. В результате, WebFlux-приложение может быть написано довольно декларативно и лаконично, без явного управления потоками или блокировками. Однако требуется некоторая практика, чтобы привыкнуть к таким цепочкам и освоить многочисленные операторы Reactor.
Обработка ошибок и завершения
В реактивном коде нет привычных try-catch прямо в последовательном потоке исполнения, потому что работа происходит отложенно. Вместо этого используются специальные операторы для обработки ошибок:
- onErrorResume(e -> …) – перехватить ошибку и подставить альтернативный Publisher.
- onErrorReturn(value) – вернуть значение по умолчанию при ошибке.
- doOnError(e -> …) – побочный эффект (например, логирование) при возникновении ошибки, без перехвата.
- timeout(Duration) – автоматически генерирует TimeoutException, если Mono/Flux не успел завершиться.
- retry(n) – повторить последовательность n раз при ошибке.
Spring WebFlux также интегрируется с механизмом контроллер-адвайсов (@ControllerAdvice) для глобальной обработки ошибок на уровне HTTP (как упомянуто в отрывке выше ). Но это скорее инфраструктура. На уровне бизнес-логики обычно используют onErrorResume и т.п.
Например, если мы вызываем внешний сервис через WebClient и хотим, чтобы при ошибке вернуть пустой результат вместо падения 500, можно сделать:
return webClient.get().uri(...).retrieve().bodyToMono(Data.class)
.onErrorResume(IOException.class, ex -> Mono.empty());
Тогда если произошел I/O Exception при вызове, мы отдаем Mono.empty() (которое WebFlux воспримет как 204 No Content или просто отсутствие тела).
Тестирование WebFlux-приложений
При тестировании реактивных компонентов часто используют StepVerifier (из Reactor Test), который позволяет “подписаться” на Mono/Flux в тесте и проверять пришедшие элементы и события (onComplete, onError). Например:
Mono<String> mono = someReactiveMethod();
StepVerifier.create(mono)
.expectNext("expectedValue")
.expectComplete()
.verify();
Для контроллеров WebFlux имеется WebTestClient, с помощью которого можно запускать тестовые HTTP-запросы и получать реактивные ответы, проверяя статус и содержимое, аналогично MockMvc в MVC, но в асинхронном стиле.
Детальное описание тестирования выходит за рамки введения, однако важно помнить: реактивные последовательности в тестах нужно подписывать либо через StepVerifier, либо блокировать (например, mono.block() внутри теста, что допустимо, ведь тест – это не production code). Иначе тест может закончиться раньше, чем асинхронная операция выполнится.
На этом завершим обзор базовых примеров. Мы создали контроллеры, вызывали внешние API параллельно, объединили результаты – все это без создания дополнительных потоков вручную и без блокировок. Теперь перейдем к сравнению WebFlux с альтернативными подходами, в частности с появлением виртуальных потоков в Java 21.
WebFlux vs виртуальные потоки: заменят ли Loom/Virtual Threads реактивный стек?
Появление виртуальных потоков (Project Loom) в Java 21 вызвало обсуждения: а нужны ли теперь все эти сложные Reactive Streams, если можно добиться того же, просто запустив каждый запрос в отдельном легком потоке? Давайте сравним эти подходы и ответим на вопрос, заменят ли виртуальные потоки WebFlux.
Сходство целей: и реактивный подход, и виртуальные потоки решают одну проблему – эффективное масштабирование под большое число одновременных операций ввода-вывода. В WebFlux мы уходим от “один запрос = один поток”, чтобы не держать тысячи потоков. В Loom мы говорим: пусть будет “один запрос = один поток”, но поток будет настолько дешевым, что тысячи потоков не проблема.
Принцип работы Loom: виртуальный поток – это абстракция над задачей, которая может приостанавливаться и возобновляться. Когда код в виртуальном потоке вызывает блокирующую операцию (например, InputStream.read() или JdbcTemplate.query()), под капотом JVM вместо блокировки ОС делает парковку виртуального потока и освобождает реальный поток-носитель (из ForkJoinPool или заданного исполнителя) для других задач. Когда результат I/O готов, виртуальный поток возвращается в очередь выполнения и продолжает с того места, где остановился. Все это делается прозрачно для программиста – код выглядит синхронно, но планировщик Loom выполняет его эффективно, многозадачно на небольшом пуле ОС-потоков. Таким образом, виртуальные потоки, как и WebFlux, ожидают I/O без блокировки системных потоков.
Преимущества виртуальных потоков:
- Простота разработки. Код остается императивным, как в старые добрые времена. Не нужно переписывать сервисы на Mono/Flux, учить операторные комбинации, беспокоиться о subscribe. Использование виртуальных потоков вообще не требует какого-то специального API (Spring MVC сам умеет выполнять контроллеры на виртуальных потоках – достаточно указать настройку). Разработчикам проще понимать и отлаживать последовательный код. Как отмечается в обсуждениях, код на MVC + виртуальные потоки выглядит естественно и очевидно, а реактивный код порой напоминает “сантехнику” (plumbing) из callback’ов и операторов, которую сложнее читать ночью в продакшене.
- Совместимость со стэком. С Loom вы можете использовать все те же библиотеки, что и раньше: JDBC, JPA, RestTemplate, etc. Они будут работать через виртуальные потоки без блокировок на уровне ОС. Например, вызов JDBC в виртуальном потоке блокирует виртуальный поток, но реальные потоки не простаивают – таким образом можно задействовать существующие драйверы БД. В WebFlux же, как мы упоминали, требуется реактивный драйвер (R2DBC) или вынос блокирующих вызовов на отдельный Scheduler. С Loom такого трюка не нужно – любой блокирующий код просто работает, не ломая масштабируемость. Это сильно упрощает интеграцию: можно брать проверенные библиотеки, которые еще не имеют реактивных аналогов, и использовать без оглядки.
- Производительность (в определенных сценариях). Бенчмарки пока дают разные результаты, но часто оказывается, что для типичных веб-приложений на базе БД Spring MVC + виртуальные потоки не уступает, а иногда и опережает WebFlux. Почему? Если у приложения много точек блокировки (БД, внешние сервисы), то WebFlux приходится переключать контексты Reactor, задействовать те же JDBC (через блокирующий мост) – все это вносит оверхед. В Loom просто создается много виртуальных потоков, каждый эффективно проводит свою работу с блокирующим драйвером. Нет сложного координирования backpressure, нет расходов на создание Flux/Mono объектов, меньше нагрузка на GC (в реактивном коде порождается много мелких объектов). Простой код = высокая производительность на CPU, особенно для простых запросов. Конечно, при экстремальном числе соединений (десятки тысяч) WebFlux на чистом Netty может показать лучше throughput, но граница существенно сдвигается благодаря Loom. В реальных кейсах 2025 года команды, мигрировавшие с WebFlux на Loom, сообщают о снижении нагрузки CPU на 40-70% и удешевлении инфраструктуры при том же RPS.
Преимущества реактивного WebFlux (что остается за ним):
- Streaming и backpressure. Если ваше приложение должно отправлять/получать беспрерывные потоки данных, WebFlux все еще очень кстати. Примеры: серверные события (SSE) на тысячи клиентов, долгоживущие WebSocket-соединения, потоковая передача файлов/медиа. В WebFlux эти сценарии встроены: Flux может быть бесконечным, backpressure гарантирует, что быстрый источник не захлестнет медленного получателя. В Loom можно реализовать нечто похожее, но придется вручную заботиться о том, чтобы не забить буфер клиента. Кроме того, Reactor имеет удобные операторы для комбинирования потоковых событий, которых нет из коробки при императивном программировании. Если нужна реактивная обработка потока событий в реальном времени (например, агрегация, фильтрация на лету) – WebFlux/Project Reactor дают готовые инструменты.
- Высококонкурентные event-driven системы. Когда количество одновременных событий становится очень большим, реактивная модель может оказаться более предсказуемой по ресурсам. Например, для миллиона соединений (случай IoT или биржевого маркет-мейкера) WebFlux может быть оптимальнее, так как не создает миллион стеков – он управляет ими в приложении. Виртуальные потоки тоже очень легкие, но их стек хоть и не полностью, но занимает память, и scheduler Loom тоже имеет накладные расходы. В отчете одного теста отмечено: по мере увеличения concurrency (100, 1000, 10000+ запросов) WebFlux начинал обгонять Virtual Threads по throughput, видимо за счет более эффективного использования CPU на очень большом количестве задач. То есть на пике экстремальной нагрузки WebFlux может быть быстрее, хотя для большинства практических случаев Loom уже “на уровне”.
- Сложные цепочки асинхронности. Если у вас сценарий, где нужна оркестрация многих асинхронных источников, преобразований, с разными комбинациями – реактивный API позволяет выразить это декларативно. Loom же склоняет к написанию параллельного кода с ExecutorService или CompletableFuture для сложных случаев, что тоже бывает непросто. Виртуальные потоки упрощают простой случай (запрос -> БД -> ответ), но они не предоставляют высокоуровневые абстракций для асинхронных потоков данных. Например, нет аналога оператора zip или flatMap над разными задачами – вам все равно придется вручную синхронизировать. Возможно, в будущем появятся утилиты для структурированной конкуренции (structured concurrency) в Java, упрощающие этот момент. Но Reactor уже сейчас предоставляет богатый инструментарий.
- Экосистема и требование реактивности сквозь. Если ваша система уже построена как набор реактивных микросервисов, общающихся через RSocket, Kafka и т.д., то WebFlux вписывается естественно. Он легко интегрируется с WebSocket, RSocket, reactive database drivers, а также с Kotlin coroutines, что тоже пользуется спросом. Виртуальные потоки решают задачу на уровне JVM, но не заменяют, скажем, реактивный драйвер для MongoDB – он остается реактивным. То есть, даже перейдя на Loom, вы можете продолжать использовать WebFlux в определенных частях, особенно там, где удобен функциональный реактивный стиль. Например, Spring Cloud Gateway (реактивный прокси) или Spring Security в реактивном режиме – их можно комбинировать с Loom, но они сами по себе реактивны внутри.
Выводы и рекомендации: Похоже, что виртуальные потоки действительно снимают основную боль, ради которой массово внедряли WebFlux – а именно проблему блокирующих потоков для I/O. Реактивность была решением проблем с потоками, теперь, когда у нас нет проблем с потоками, реактивность уже не обязательна для масштабируемости. Многие эксперты полагают, что для 80-90% веб-приложений на Spring виртуальные потоки + Spring MVC станут новым стандартом (де-факто заменив WebFlux по умолчанию). WebFlux останется инструментом для специальных случаев: реального времени, стриминга, огромной конкурентности и интеграции с специфическими реактивными источниками данных.
Spring-разработчики уже двигаются в эту сторону: Spring Boot добавил экспериментальную поддержку Loom (флаг spring.threads.virtual.enabled=true включает выполнение контроллеров MVC на виртуальных потоках, если использовать Tomcat/Jetty). При этом сам WebFlux тоже развивается – Project Reactor интегрируют с Loom (появится планировщик, умеющий использовать виртуальные потоки, чтобы облегчить mix & match). Так что, возможно, разделение начнет стираться: вы сможете писать императивный код, но при желании втыкать реактивные Flux для конкретных участков (например, для обработки веб-сокета), и все это будет сосуществовать.
Если прямо ответить на вопрос “заменят ли виртуальные потоки WebFlux?” – скорее нет, не полностью. Virtual Threads сильно потеснят реактивные фреймворки в области обычных CRUD/REST приложений (там WebFlux теперь действительно не дает существенных преимуществ, а код усложняет). Однако реактивный подход не умрет: он по-прежнему необходим в случаях, где нужна управляемая реактивность (backpressure) или где хочется писать декларативный асинхронный pipeline. Кроме того, WebFlux/Reactor – это не только про HTTP запросы, но и про обработку событий (например, поток сообщений из Kafka можно обрабатывать как Flux с backpressure – Loom здесь не поможет, так как reading from Kafka itself asynchronous and we need control rate). Так что разработчикам стоит изучить и понимать оба подхода и применять там, где уместно.
В целом же, с появлением Loom выбор стал проще: начните с Spring MVC + виртуальные потоки (JDK 21+) для нового приложения – вы получите отличную производительность без лишней сложности. И только если обнаружите, что нужен стриминг или система событий, подумайте о реактивном WebFlux для этих конкретных компонентов. Ниже мы структурируем эти рекомендации.
Рекомендации: когда использовать WebFlux, а когда достаточно Spring MVC
Наконец, сведем все выше сказанное к практическим рекомендациям. Нет “единственно правильного” решения, выбор зависит от характера вашего приложения. Рассмотрим случаи, когда WebFlux действительно раскрывает свои преимущества, и случаи, когда он излишне усложняет жизнь.
Когда WebFlux оправдан и полезен:
- Очень высокая конкуренция подключений или интенсивный I/O. Если ваше приложение должно одновременно обслуживать тысячи и десятки тысяч одновременных запросов, большинство из которых ожидают внешние ресурсы (БД, API, файловые хранилища), реактивная модель позволит масштабироваться эффективнее, чем традиционная. WebFlux специально спроектирован для сценариев с высоким числом одновременных пользователей без раздувания пула потоков.
- Реал-тайм обновления и потоки данных. Примеры: системы с протоколами веб-сокетов, серверными событиями (SSE), live-стримингом данных (биржевые котировки, онлайн-игры, чат-серверы). WebFlux shines in scenarios where you have continuous data streams or need instant push updates to clients. За счет неблокирующей обработки и Flux, он упрощает отправку данных по мере готовности и управление потоком событий. Такие вещи как push-уведомления, многопользовательские чаты, спортивные счеты в реальном времени – все это случаи для WebFlux.
- Микросервисная архитектура с множеством внешних вызовов. В экосистеме микросервисов ваш сервис может выступать оркестратором, дергая 5-10 других сервисов для одного входящего запроса. WebFlux поможет параллелить эти вызовы и не блокировать поток под каждый. В то время как Spring MVC вынужден бы выполнять их последовательно или через сложно настраиваемый @Async. WebFlux позволит асинхронно запускать все внешние запросы и агрегировать результаты, что особенно полезно при большом количестве межсервисных взаимодействий.
- I/O-bound задачи с длительным ожиданием. Если ваше приложение делает множество операций ввода-вывода (запросы к БД, файлам, другим API) и при этом сами вычисления быстрые, WebFlux обеспечит, что ни один поток не простаивает. Например, gateway сервис, пробрасывающий запросы, или приложение, сильно зависящее от внешних API. Операции ввода-вывода не блокируют основные потоки и приложение использует CPU более рационально.
- Событийно-ориентированные системы, реактивные стримы. Если вы строите приложение вокруг брокеров сообщений (Kafka, RabbitMQ) или обрабатываете большие потоки событий, WebFlux/Project Reactor дают отличные инструменты. Вы сможете применять backpressure к потокам сообщений, реактивно трансформировать их, объединять с веб-слоем. Event-driven архитектуры получают выгоду от реактивности, так как природно асинхронны.
- Интеграция с современными реактивными технологиями. Некоторые новые проекты сразу ориентированы на реактивность: например, MongoDB имеет реактивный драйвер, Redis – тоже, R2DBC для SQL, GraphQL subscription – реактивны по сути. Если стек вашей системы уже использует эти, то WebFlux вписывается естественно. Вы сможете end-to-end пройтись реактивными стримами без адаптеров.
- Максимальное использование ресурсов при вертикальном масштабировании. Когда нужно выжать максимум из одного инстанса (например, мощного сервера) под нагрузкой, реактивность зачастую показывает лучшее использование CPU и памяти на единицу throughput. Особенно при вертикальном масштабировании (увеличении мощностей одного сервера) реактивная модель может эффективно задействовать каждый ядро, не теряя на блокировках.
Когда WebFlux не нужен и даже нежелателен:
- Низкая или умеренная нагрузка, простые приложения. Если у вас типичный веб-сайт или REST API с предсказуемым невысоким трафиком (сотни запросов в секунду, а не тысячи), то реактивность будет избыточна. Spring MVC справится прекрасно, а WebFlux только усложнит проект. Нет смысла вводить дополнительную сложность ради масштабирования, которое вам не требуется.
- CPU-bound задачи, интенсивные расчеты. WebFlux не ускоряет вычисления на CPU. Если ваша система занимается тяжелыми вычислениями (мат. моделирование, обработка изображений, большие агрегаты данных), то реактивность не даст прироста, а может и ухудшить ситуацию. Более того, в WebFlux по умолчанию все обработчики работают на нескольких общих потоках Event Loop – если один запрос начнет грузить CPU (например, сортировать огромный массив), он будет мешать обработке других запросов на том же потоке. В MVC же такие задачи распределяются по разным потокам из пула. Идея WebFlux – эффективно ждать I/O, а не быстрее крутить CPU. Поэтому для CPU-bound лучше либо традиционный подход, либо выносить такие задачи в отдельные воркеры/пулы (что можно сделать и с MVC, и с WebFlux, но с MVC хотя бы проще – потоки уже изолированы).
- Высокий порог вхождения и сложность для команды. Реактивное программирование требует другого образа мыслей. Не всем разработчикам это дается легко. Если команда не имеет опыта с Mono/Flux, обучение и постепенное привыкание займет время. Код на WebFlux сложнее дебажить – стек вызовов не отображает последовательность напрямую, нужно читать сигналы onNext/onError. Инструменты профилирования и отладки тоже менее развиты для реактивного стека. Поэтому, если проект простой, сроки сжатые, а команда не знакома с реактивностью, WebFlux может больше навредить, чем помочь. Лучше использовать проверенный синхронный подход – так вы быстрее разработаете функционал и снизите риски ошибок. (Отметим, Loom в этом плане сильно снижает актуальность WebFlux – вы получаете масштабируемость без новой парадигмы, так что фактор обучения становится решающим.)
- Ограниченная поддержка библиотек. Не вся экосистема Java успела стать реактивной. Например, стандартные JPA/Hibernate пока не работают в реактивном режиме (нужны альтернативы типа R2DBC, которые функционально пока беднее). Множество сторонних библиотек (SDK облачных сервисов, старые драйверы) не имеют реактивных аналогов. Используя WebFlux, вы можете столкнуться с ситуацией, что нужная вам библиотека блокирующая – придется городить “обертки” на Schedulers.boundedElastic() для ее вызовов, теряя преимущества. Или вовсе выбрать менее удобный инструмент ради реактивности (например, отказаться от Hibernate в пользу простого R2DBC Template, жертвуя кэшем 2-го уровня, JPQL и т.д.). Если ваш проект сильно зависит от таких blocking-only библиотек, возможно, рациональнее остаться на MVC, либо тщательно взвесить, готовы ли вы мириться с ограничениями. (Виртуальные потоки опять-таки решают эту проблему – можно использовать блокирующую библиотеку на виртуальном потоке без вреда для масштабируемости.)
- Существующий код и интеграции. Если у вас уже есть большой кодовая база на Spring MVC, переводить ее на WebFlux ради гипотетических выигрышей – рискованно и трудоемко. Реактивность “не распространяется частично” – вам пришлось бы переписать почти все на новый лад, включая сервисы, репозитории, фильтры, безопасность. Частичный переход возможен (например, один модуль на WebFlux, другой на MVC, в рамках одного приложения через раздельные контексты), но это сложно. Проще, если нужно масштабирование, вынести узкие места во внешние сервисы или применить ту же вертикальную/горизонтальную масштабируемость. В общем, не переписывайте существующее на реактив, если только действительно не уперлись в потолок и все другие меры не помогли.
- Отладка и профилирование. Этот пункт вытекает из сложности, но заслуживает упоминания: если ваше приложение предполагается поддерживать долго и активно улучшать, учтите, что поддержка реактивного кода может быть сложнее. Стек вызовов при исключении в Mono легко уходит внутрь Reactor, и понять, где ошибка, бывает труднее. Логирование асинхронное, контекст исполнения “скачет” (MDC в реактивном коде поддерживается, но не автоматически – нужен Reactor Context). Не все APM-инструменты полноценно поддерживают реактивные трассы (хотя многие уже научились). Если для вас крайне важно иметь прозрачную поддержку и минимальный technical risk, традиционный стек может быть предпочтительнее.
Роль виртуальных потоков: как мы обсудили в предыдущем разделе, Loom изменил баланс. Ранее рекомендация была: используйте WebFlux для high-load I/O, MVC – для всего остального. Сейчас же появился третий вариант: Spring MVC + Virtual Threads покрывает большинство high-load I/O сценариев без WebFlux. Поэтому новая картина примерно такая:
- Для новых проектов на Java 21+: Сначала пробуйте Spring MVC с виртуальными потоками. Вы получите простоту и масштабируемость. Если нужна асинхронщина – можно использовать CompletableFuture или WebClient внутри контроллеров, Loom все равно эффективно это обработает.
- Используйте WebFlux только если: вам нужны особенности реактивности (SSE, WebSocket, backpressure), либо вы заведомо создаете систему на реактивных источниках (например, полностью реактивный pipeline с Kafka и R2DBC), либо у вас очень жесткие требования по производительности/памяти, и вы измерили, что WebFlux выигрывает.
- Для существующих реактивных приложений: не спешите их переписывать обратно на MVC. Они, конечно, стали чуть менее уникальными, но если все работает – продолжайте, где надо, можно внутри использовать Loom (Spring 6 умеет запускать реактивные обработчики на Loom scheduler с Reactor 3.6). В будущем, возможно, произойдет слияние подходов.
- Для legacy приложений MVC с проблемами масштабирования: сначала попробуйте просто обновиться до Java 21 и включить виртуальные потоки – скорее всего, этого хватит. Если же и этого мало (например, у вас приложение, вызывающее множество API, и оно не масштабируется по причине переключения контекста или ожидания) – возможно, пора задуматься об архитектурных изменениях или уже тогда о переходе на WebFlux, но это крайняя мера.
Подытоживая: WebFlux – мощный инструмент для решения определенного круга задач, но не серебряная пуля. С приходом новых возможностей Java его ниша сузилась, и теперь разработчикам нет нужды использовать реактивность ради самой реактивности. Решение должно диктоваться требованиями: если у вас типичный веб-сервис с REST CRUD – смело берите Spring MVC (особенно с Loom). Если же вы делаете высоконагруженный realtime-сервис или gateway, где на каждое событие приходится десяток асинхронных операций – WebFlux покажет себя во всей красе. WebFlux – не замена Spring MVC, а другой инструмент для других задач.
Заключение
Spring WebFlux привносит в экосистему Spring идеологию реактивного программирования – когда мы реагируем на поступление данных, а не ждем их, блокируя ресурсы. Мы разобрали, почему эта модель возникла: традиционный “поток на запрос” плохо масштабируется под нагрузки с большим числом одновременных соединений, типичных для современных веб-приложений и микросервисов. WebFlux решает проблему, позволяя обслуживать тысячи запросов с помощью горстки потоков, благодаря неблокирующему I/O и эффективному управления потоком событий (Reactive Streams, backpressure). Мы погрузились в основы Reactor – Mono, Flux, operators – и увидели на примерах, как строить реактивные цепочки вместо последовательных блокирующих вызовов.
Для начинающего разработчика переход к реактивному мышлению – серьезный шаг. В этой статье мы постарались смягчить порог входа, объяснив концепции простыми словами и демонстрируя типичные паттерны кода. Мы также рассмотрели новейший тренд – виртуальные потоки Java – и выяснили, что хотя они уменьшают потребность в WebFlux для ряда задач, реактивный подход остается незаменимым в ситуациях стриминга данных и экстремальной масштабируемости. В итоге, инструменты должны служить задачам: WebFlux хорош там, где нужны его преимущества, и излишен там, где простой синхронный код справится не хуже.
Главный совет: не выбирать архитектуру по принципу “модности” или “новизны”. Spring WebFlux – замечательная технология, но применять ее стоит осознанно. Если вы решите использовать WebFlux, убедитесь, что выгоды (масштабируемость, скорость на I/O, стриминг) перевешивают издержки (сложность кода, порог входа для команды, ограничения библиотек).
![]()
You must be logged in to post a comment.