В сегодняшней статье мы рассмотрим тему, которая на первый взгляд кажется довольно тривиальной для программистов, не связанных с миром реактивного программирования. Я говорю о «попробуй-поймай-наконец-то».

Большинство языков программирования предлагают ключевые слова для этой цели, поэтому их очень легко реализовать и использовать. В блоке try мы выполняем некоторую операцию, которая может завершиться ошибкой и вызвать исключение. Чтобы предотвратить завершение приложения с неожиданной ошибкой на этом этапе, мы можем защитить операцию с помощью блока try, чтобы соответствующим образом обработать ошибку. Теоретически мы можем защитить любую операцию в блоке try. Однако на практике мы часто работаем с некоторыми ресурсами, которые необходимо обработать в конце обработки, например, закрытие входных потоков или файлов, к которым мы обращались. В блоке catch мы перехватываем исключения, сгенерированные в блоке try, и обрабатываем их соответствующим образом. И, наконец, в необязательном блоке finally мы выполняем некоторую работу по очистке, например, закрываем ресурс, к которому мы обращались в блоке try. Пока довольно просто.

Однако в реактивном мире мы не можем просто генерировать исключение в случае сбоя, где-то его перехватывать и, возможно, потом подчищать, независимо от того, не удалось выполнить выполнение или нет. Любые данные и, следовательно, любые исключения должны следовать за последующей обработкой в ​​реактивной цепочке. Вырваться из реактивной цепочки означает вырваться из любой выгоды, которую дает реактивное программирование. Поэтому важно знать о соответствующем представлении ошибок и шаблонов, чтобы обрабатывать их в реактивном мире.

Я остановлюсь на функционале, предоставляемом Mono.using() из фреймворка Project Reactor. Поэтому заглянем в соответствующий JavaDoc:

Использует ресурс, сгенерированный поставщиком для каждого отдельного подписчика, одновременно передавая значение из Mono, полученного из того же ресурса, и обеспечивает освобождение ресурса, если последовательность завершается или подписчик отменяет.

Внимательный читатель мог понять, что это более или менее отдельные шаги try-catch-finally (или фактически try-with-resources), только перенесенные в реактивный мир.

Поставщик создает ресурс, и над этим ресурсом выполняется некоторая обработка путем применения функции поставщика (→ try). Затем этот ресурс очищается функцией-потребителем для начального ресурса ( → finally). Единственное, что мы тут не находим сразу, — это отлов исключений.

Однако, поскольку исключения преобразуются в сигналы об ошибках, мы также можем реализовать «блок перехвата», реагируя на эти сигналы с помощью соответствующих методов в реактивной цепочке.

В недавнем проекте мы хотели загрузить файлы журналов с сервера, а затем обработать их. Позже эти файлы журналов следует очистить, чтобы дескрипторы файлов не оставались открытыми и в файловой системе не оставалось временных файлов. Точнее, мы хотели загрузить журналы выполнения из экземпляра Ansible Tower для наших выполненных заданий Ansible. В этом контексте у меня была возможность использовать реактивный паттерн «попробуй-поймай-наконец-то». Рабочий процесс включает в себя следующие шаги:

  • Сначала создается временный файл. Это наш ресурс.
  • Затем загружаем логи из Ansible Tower, записываем во временный файл и обрабатываем результат.
  • В конце мы хотим удалить временный файл в любом случае.
  • Если произойдет что-то неожиданное, мы хотим обработать исключение и повторно сгенерировать его как собственное исключение.

Это может быть реализовано следующим образом:

Вся часть со 2-й по 23-ю строку — это «попытка-наконец-то».

Рассмотрим подробнее отдельные компоненты метода generateJobLog, начиная с поставщика ресурсов:

Во-первых, временный файл создается путем передачи функции поставщика ресурсов методу. Когда этот вызов завершится успешно, результат будет передан функции исходного поставщика.

Вызов неблокирующего WebClient доставляет нам поток DataBuffers, абстракции Spring над байтовыми буферами. Файл журнала загружается по частям, чтобы свести к минимуму нагрузку на память. Однако теперь мы получаем Flux<DataBuffer>, который нам нужно обработать для дальнейшей обработки.

DataBufferUtils предоставляют несколько действительно удобных служебных методов для обработки буферов данных без проблем. Здесь мы используем DataBufferUtils.write, который принимает поток буферов данных и по частям записывает его в заданный файл. Аккуратный!

Плоско отображая результат операции DataBufferUtils, мы можем быть уверены, что файл существует и содержит журнал заданий. Теперь мы можем обработать данные журнала.

После обработки фрагментов данных и дальнейшей обработки заполненных данных ресурс передается потребителю очистки ресурсов, который удаляет файл, если он существует.

Любое возникающее исключение в части Mono.using будет правильно распространяться как сигнал ошибки по реактивной цепочке. Следовательно, часть «поймать» в «попытаться поймать-наконец» эмулируется с помощью onErrorMap.

В этом варианте onErrorMap берет функцию, которая использует выброшенное исключение, и сопоставляет ее с другим исключением по выбору.

Существует множество других доступных методов обработки ошибок. Не стесняйтесь просматривать JavaDoc для Mono или Flux.

Для сравнения я хотел показать вам реализацию с блокировкой RestTemplate (вместо WebClient) и еще через «try-catch-finally»:

Структура примерно такая же. Нам понадобятся два блока try, чтобы обеспечить очистку временного файла в самом конце обработки. Вместо Flux или буферов данных мы работаем с ответом от RestTemplate напрямую, определяя ResponseExtractor в строке 11. Но помимо этого различия относительно невелики. За исключением, конечно, того, что реактивный вариант неблокирующий, а здесь блокирующий.

Как вы видели, работать с ресурсами в реактивной среде не так уж и сложно. Просто попробуйте сами!

Спасибо за прочтение! Не стесняйтесь комментировать или сообщение мне, когда у вас есть вопросы или предложения. Вас могут заинтересовать другие посты, опубликованные в Блоге Digital Frontiers, анонсированные в нашем аккаунте Twitter.