Как работать с CompletableFuture в Spring?
Введение
В современном мире приложения становятся все более сложными и функциональными. Чтобы они работали быстро и не тормозили, программисты используют разные техники. Одна из таких техник - это асинхронное программирование. Оно позволяет увеличить скорость работы программы за счет того, что некоторые задачи выполняются параллельно.
Одним из популярных фреймворков для создания современных веб-приложений на Java является Spring. А для реализации асинхронности в Spring программисты часто используют библиотеку CompletableFuture.
Как работать с CompletableFuture в Spring
Давай начнем с самого простого - как создать CompletableFuture. Это в принципе легко. Нужно просто вызвать статический метод completedFuture() и передать туда результат, который нужно вернуть в будущем.
CompletableFuture<String> future = CompletableFuture.completedFuture("Hello");
Здесь мы создали CompletableFuture, который сразу же возвращает строку "Hello".
Также можно использовать метод supplyAsync(), чтобы отправить задачу на asynchronous выполнение:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // какая-то задача return "Hello"; });
В этом случае результат "Hello" будет возвращен не сразу, а когда asynchronous задача завершится.
Ок, давай теперь разберем, как создавать асинхронные задачи. Самый простой способ - использовать методы supplyAsync() и runAsync().
Метод runAsync() запускает задачу асинхронно, но ничего не возвращает:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { // какая-то задача });
А вот supplyAsync(), как мы видели выше, возвращает результат:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // какая-то задача return "результат"; });
Эти методы можно вызывать с Executor'ом, чтобы указать пул потоков для выполнения задачи. Например:
Executor executor = Executors.newFixedThreadPool(10); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // какая-то задача }, executor);
Вот так мы отправляем асинхронную задачу в нужный нам пул потоков. Круто, да?
Дальше посмотрим, как комбинировать задачи. Это очень мощная штука в CompletableFuture!
Можно последовательно выполнять задачи с помощью thenApply(), thenAccept(), thenRun(). Рассмотрим на примере:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { return "результат 1"; }); CompletableFuture<String> future2 = future1.thenApply(res -> { return "результат 2"; });
Сначала выполнится future1 и вернет "результат 1". Затем запустится задача в thenApply, принимающая результат future1, обработает его и вернет "результат 2".
Аналогично работают thenAccept() и thenRun() - просто без возврата результата.
Еще один способ - комбинировать два CompletableFuture с помощью thenCombine():
CompletableFuture<String> future1 = // ... CompletableFuture<String> future2 = // ... CompletableFuture<String> resultFuture = future1.thenCombine(future2, (res1, res2) -> res1 + " " + res2));
Как только future1 и future2 завершатся, запустится задача thenCombine, которая принимает их результаты и возвращает комбинацию. Очень удобно!
Теперь давай разберемся с ошибками. В асинхронных задачах они могут возникать, поэтому нужно правильно обрабатывать.
Для этого есть методы exceptionally() и handle():
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // код который может бросить исключение }).handle((res, ex) -> { if(ex != null) { return "что-то пошло не так"; } return res; });
В handle() мы проверяем, было ли исключение, и возвращаем результат или сообщение об ошибке.
Метод exceptionally() работает похоже, но только для исключений:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // код который может бросить исключение }).exceptionally(ex -> { return "что-то пошло не так"; });
Вот так мы перехватываем ошибки в асинхронных задачах.
А как получить результат задачи, когда она завершится? Для этого есть методы join() и get().
join() ждет завершения задачи и возвращает результат или выбрасывает исключение:
String result = future.join();
А get() делает то же самое, но может сам бросить исключение:
String result = future.get();
Нужно обрабатывать это исключение try-catch.
Хорошо, а теперь пришло время разобраться с многопоточностью и параллелизмом.
CompletableFuture позволяет запускать асинхронные задачи параллельно на разных потоках. Это значительно ускоряет выполнение за счет использования всех ядер процессора.
Мы уже видели, как передать Executor для выполнения задачи асинхронно.
Но есть еще один крутой способ - использовать метод allOf(), чтобы запустить сразу несколько задач:
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> { // задача 1 }); CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> { // задача 2 }); CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2);
Тут мы запускаем future1 и future2 параллельно на разных потоках. В combinedFuture они объединяются.
Это позволяет эффективно распараллелить вычисления для ускорения программы. Круто, правда?
Иногда бывает нужно отменить задачу, если она больше не требуется или занимает слишком много времени.
Для этого в CompletableFuture есть метод cancel(). Он пытается прервать выполнение задачи:
future.cancel(false);
Параметр false означает, что прерывание будет "мягким" - позволит задаче завершиться самостоятельно. А true - "жесткое" прерывание, может привести к исключениям.
Но лучше избегать жесткого прерывания, чтобы не получить непредсказуемое состояние приложения.
Для отслеживания прогресса асинхронной задачи в CompletableFuture есть метод thenAcceptBoth(). Он вызывается периодически и позволяет обновлять прогресс:
future.thenAcceptBoth(otherFuture, (res1, res2) -> { // код для обновления прогресса });
Мы передаем сюда другой CompletableFuture, чтобы комбинировать результаты. Это позволяет гибко управлять прогрессом.
Что в итоге?
Мы разобрали некоторые основы работы с CompletableFuture в Spring для асинхронного программирования. Как видишь, инструмент неплохой. Он позволяет создавать асинхронные задачи, комбинировать их, обрабатывать ошибки, делать параллельно - словом, асинхронность крутая штука. Благодаря CompletableFuture ты можешь сделать свое приложение более отзывчивым и масштабируемым. Так что, имеет смысл обратить на него внимание.