В сегодняшней статье мы рассмотрим тему, которая на первый взгляд кажется довольно тривиальной для программистов, не связанных с миром реактивного программирования. Я говорю о «попробуй-поймай-наконец-то».
Большинство языков программирования предлагают ключевые слова для этой цели, поэтому их очень легко реализовать и использовать. В блоке 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.