Реактивное программирование уже давно начало свой путь от чистых веб-приложений к приложениям Java Enterprise. Он обещает более высокую производительность и меньший объем памяти за счет использования асинхронных неблокирующих вызовов.
Скорее всего, вы уже читали документацию и сообщения в блогах о реактивном программировании или одной из его реализаций на Java. В этой серии статей я не собираюсь вдаваться в подробности того, как программировать с использованием одной из существующих Reactive framework. Я лучше объясню, что на самом деле означает использование реактивного программирования и какие препятствия будут ждать вас. Обычные вещи, такие как отладка, тестирование и обработка исключений, требуют новых или иных подходов, поскольку асинхронность имеет свою цену. Эта тема часто скрывается и резюмируется только словами: «Кривая обучения от императивного к реактивному - крутая». Вот почему я посвящаю эту серию этим темам.
Примечание. Примеры будут написаны на Котлине. Не бойтесь, если вы не знакомы с котлином. Примеры легко понять и выглядят очень похожими на разных языках. Для первого знакомства с языком вы также можете взглянуть на сообщение в блоге моего коллеги: 7 вещей, которые должен знать любой Java-разработчик, начиная с Kotlin
Как абсолютный энтузиаст Spring Framework, я использую фреймворк Spring Reactor, который широко реализует спецификацию Reactive Streams и веб-фреймворк реактивного стека Spring Webflux.
Давайте просто создадим все реактивное
Иногда в обсуждениях кажется, что реактивное программирование - это новый спаситель с небес программирования, который решит все проблемы. Я хочу с самого начала прояснить одну вещь: реактивное программирование - не новая серебряная пуля. Я бы не рекомендовал его для стандартного бэкэнд-приложения Java и повседневных проектов. Реактивное программирование начинает проявляться, когда вашему приложению требуется обслуживать десятки тысяч одновременных пользователей, и вы просто не можете обрабатывать каждый входящий запрос в своем собственном потоке веб-сервлета, поскольку потоки в Java недешевы.
Большинство определений реактивного программирования описывают, что оно обрабатывает потоки данных и занимается распространением изменений. Поскольку это никогда не значило для меня ничего полезного, я исследовал и наткнулся на следующее описание Codingame, которое было намного легче понять:
«Реактивное программирование - это новая парадигма, в которой вы используете декларативный код (аналогично функциональному программированию) для построения конвейеров асинхронной обработки. Это модель, основанная на событиях, в которой данные передаются потребителю по мере того, как они становятся доступными: мы имеем дело с асинхронными последовательностями событий. »
Реактивный код собирается вокруг асинхронных функциональных цепочек, где входные данные передаются (распространяются) через эти цепочки от производителя к подписчикам.
Итак, самый важный вопрос: какую проблему на самом деле решает реактивное программирование?
Разочаровывающий ответ заключается в том, что все, что вы можете решить с помощью реактивного программирования, можно решить и с помощью других подходов. Нет никакого реального «Я если у вас есть проблема X, просто реагируйте, и все будет хорошо». Даже если вы работаете с потоками данных и последовательностями событий, реактивное программирование не обязательно лучше всего подходит. Для неопытного читателя код будет выглядеть еще более нечитабельным из-за уровня абстракции, обеспечиваемого реактивным кодом.
Но по той же причине код, скорее всего, будет более устойчивым и расширяемым. Такие проблемы, как обратное давление, логика повторных попыток и общая обработка асинхронных неблокирующих вызовов, отвлечены от вас. Повторить вызов службы так же просто, как добавить retry()
в цепочку выполнения.
Имейте в виду разницу между устаревшим циклом for с явным счетчиком и абстракцией foreach:
Различаются варианты абстракции переднего отдела:
В конечном итоге все делают то же самое, но логика итерации и присваивания абстрагируются с помощью foreach. Даже если вам действительно нужен индекс элемента, что случается очень редко, вы можете просто использовать forEachIndexed
. Следующий элемент списка с его индексом будет предоставляться на каждой итерации. Однако самое большое преимущество не очевидно. Эти абстракции были реализованы и протестированы и используются тысячами разработчиков без каких-либо проблем. То же самое относится и к абстракциям реактивных библиотек для обработки потоков данных. Такие фреймворки, как RxJava и Spring Reactor, доказали свою работоспособность, тщательно протестированы и предлагают такой высокий уровень абстракции. Итак, если вы работаете с потоками данных и асинхронными последовательностями событий, подумайте об использовании какой-нибудь надежной реактивной реализации.
Ядро: реактивные потоки
Основным элементом реактивных потоков является шаблон Издатель-подписчик, в отличие от знакомого шаблона Iterable-Iterator, основанного на вытягивании.
Издатели создают значения и уведомляют подписчика о новых доступных значениях. По сравнению с потоком программы Iterator-Iterable, это как раз наоборот. Значения передаются от издателя подписчикам, а не извлекаются подписчиком.
«Издатель - это поставщик потенциально неограниченного количества упорядоченных элементов, публикующий их в соответствии с требованиями, полученными от своих подписчиков.
Издатель может обслуживать нескольких подписчиков, на которые подписаны динамически в различные моменты времени ».
Издатели могут иметь разную мощность в зависимости от того, что они на самом деле представляют. В следующих примерах используется Spring Reactor, реализация спецификации Reactive Streams. Reactor предоставляет составные реактивные типы, реализующие интерфейс Publisher, в зависимости от желаемой мощности: Mono и Flux.
В то время как Flux описывает потоки данных с количеством элементов 0..n, Mono представляет результаты с количеством элементов 0..1. За некоторыми исключениями, все операторы работают с обоими реактивными типами.
В следующем примере показан поток в форме потока Twitter в действии:
Подобно функциональному программированию, логика программы выражается декларативно. Мы описываем результат выполнения (нам нужен отфильтрованный и преобразованный список твитов), а не способ получения результата.
Помимо обработки каждого отправленного значения, подписчик также может реагировать на сигналы ошибки или сигнал завершения:
Flux и Mono могут быть преобразованы друг в друга в зависимости от операции. Если Mono генерирует несколько результатов, тип результата преобразуется в Flux:
Mono создается с нуля путем вызова Mono.just
. Затем Mono преобразуется в Flux путем применения простой факторизации к обернутому значению. Затем простые множители собираются и объединяются в строку, снова генерируя Mono. Наконец, для объекта Mono вызывается subscribe и печатается строка результата.
На этом завершается первый раздел этой серии сообщений в блоге о реактивном программировании. В этой части я познакомил вас с реактивным программированием и обсудил, когда использовать реактивное программирование, а когда нет. Используя несколько примеров кода, я показал вам общий рабочий процесс реактивных конвейеров и основных элементов фреймворка Spring Reactor.
В следующих частях этой серии я покажу вам ожидаемые трудности при обработке и написании реактивного кода, а также какие альтернативные шаблоны необходимы для повседневной разработки.
Спасибо за прочтение! Не стесняйтесь комментировать или писать мне, если у вас есть вопросы или предложения.