Аннотация @Async в Spring позволяет создавать асинхронные методы в Spring. Давайте рассмотрим @Async
в этом учебнике по Spring Framework. Кратко, когда мы аннотируем метод бина @Async
, Spring выполнит его в отдельном потоке, и вызывающий метод не будет ждать завершения выполнения метода. В этом примере мы определим собственный сервис и будем использовать Spring Boot 2. Давайте начнем!
Пример Spring @Async
Мы будем использовать Maven для создания образца проекта для демонстрации. Чтобы создать проект, выполните следующую команду в каталоге, который вы будете использовать в качестве рабочей области:
mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Если вы запускаете Maven в первый раз, выполнение команды generate может занять несколько секунд, потому что Maven должен загрузить все необходимые плагины и артефакты для выполнения этой задачи. Вот как выглядит создание проекта: После создания проекта вы можете открыть его в своей любимой среде разработки. Следующим шагом будет добавление соответствующих зависимостей Maven в проект. Вот файл
pom.xml
с соответствующими зависимостями:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Наконец, чтобы понять, какие JAR-файлы добавлены в проект при добавлении этой зависимости, мы можем выполнить простую команду Maven, которая позволяет нам увидеть полное дерево зависимостей для проекта при добавлении некоторых зависимостей. Вот команда, которую мы можем использовать:
mvn dependency:tree
При выполнении этой команды она покажет нам следующее дерево зависимостей:
Включение поддержки асинхронности
Включение поддержки асинхронности – это всего лишь вопрос одной аннотации. Помимо включения выполнения асинхронных операций, мы также воспользуемся Executor, который позволит нам определить ограничения по потокам. Подробнее об этом, когда мы напишем код:
package com.journaldev.asynchexample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@SpringBootApplication
@EnableAsync
public class AsyncApp {
...
}
Здесь мы воспользовались аннотацией @EnableAsync
, которая позволяет Spring выполнять асинхронные методы в фоновом пуле потоков. Далее, мы также добавляем упомянутый Executor:
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("JDAsync-");
executor.initialize();
return executor;
}
Здесь мы устанавливаем, что максимум 2 потока должны работать параллельно, а размер очереди установлен в 500. Вот полный код класса с импортами:
package com.journaldev.asynchexample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@SpringBootApplication
@EnableAsync
public class AsyncApp {
public static void main(String[] args) {
SpringApplication.run(AsyncApp.class, args).close();
}
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("JDAsync-");
executor.initialize();
return executor;
}
}
Далее мы создадим сервис, который фактически будет использовать выполнение в потоке.
Создание модели
Мы будем использовать общедоступный API фильмов, который просто возвращает данные о фильме. Мы определим нашу модель для этого:
package com.journaldev.asynchexample;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {
private String title;
private String producer;
// стандартные геттеры и сеттеры
@Override
public String toString() {
return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
}
}
Мы использовали @JsonIgnoreProperties
, чтобы если в ответе есть больше атрибутов, они могли быть безопасно проигнорированы Spring’ом.
Создание сервиса
Пришло время определить наш сервис, который будет использовать упомянутый API кинофильмов. Мы будем использовать простой RestTemplate для вызова GET API и получения результатов асинхронно. Давайте посмотрим на пример кода, который мы используем:
package com.journaldev.asynchexample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Service
public class MovieService {
private static final Logger LOG = LoggerFactory.getLogger(MovieService.class);
private final RestTemplate restTemplate;
public MovieService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async
public CompletableFuture lookForMovie(String movieId) throws InterruptedException {
LOG.info("Looking up Movie ID: {}", movieId);
String url = String.format("https://ghibliapi.herokuapp.com/films/%s", movieId);
MovieModel results = restTemplate.getForObject(url, MovieModel.class);
// Искусственная задержка в 1 секунду для демонстрационных целей
Thread.sleep(1000L);
return CompletableFuture.completedFuture(results);
}
}
Этот класс является @Service
, что делает его пригодным для сканирования компонентов Spring. Возвращаемый тип метода lookForMovie
– CompletableFuture
, что является требованием для любого асинхронного сервиса. Поскольку время выполнения API может изменяться, мы добавили задержку в 2 секунды для демонстрации.
Создание Command Line Runner
Мы будем запускать наше приложение с помощью CommandLineRunner, который является самым простым способом протестировать наше приложение. CommandLineRunner запускается сразу после инициализации всех бинов приложения. Давайте посмотрим на код CommandLineRunner:
package com.journaldev.asynchexample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Component
public class ApplicationRunner implements CommandLineRunner {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationRunner.class);
private final MovieService movieService;
public ApplicationRunner(MovieService movieService) {
this.movieService = movieService;
}
@Override
public void run(String... args) throws Exception {
// Запуск часов
long start = System.currentTimeMillis();
// Начать несколько асинхронных запросов
CompletableFuture page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
CompletableFuture page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
CompletableFuture page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");
// Дождитесь завершения всех потоков
CompletableFuture.allOf(page1, page2, page3).join();
// Вывести результаты, включая затраченное время
LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
LOG.info("--> " + page1.get());
LOG.info("--> " + page2.get());
LOG.info("--> " + page3.get());
}
}
Мы просто использовали RestTemaplate для вызова примерного API с некоторыми случайно выбранными идентификаторами фильмов. Мы будем запускать наше приложение, чтобы посмотреть, какой вывод он покажет.
Запуск приложения
При запуске приложения мы увидим следующий вывод:
2018-04-13 INFO 17868 --- [JDAsync-1] c.j.a.MovieService : Looking up Movie ID: 58611129-2dbc-4a81-a72f-77ddfc1b1b49
2018-04-13 08:00:09.518 INFO 17868 --- [JDAsync-2] c.j.a.MovieService : Looking up Movie ID: 2baf70d1-42bb-4437-b551-e5fed5a87abe
2018-04-13 08:00:12.254 INFO 17868 --- [JDAsync-1] c.j.a.MovieService : Looking up Movie ID: 4e236f34-b981-41c3-8c65-f8c9000b94e7
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : Elapsed time: 4056
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='My Neighbor Totoro', producer='Hayao Miyazaki'}
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='Castle in the Sky', producer='Isao Takahata'}
2018-04-13 08:00:13.566 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='Only Yesterday', producer='Toshio Suzuki'}
Если вы обратите внимание, то в приложении запускаются всего два потока, а именно JDAsync-1
и JDAsync-2
.
Заключение
В этом уроке мы изучили, как использовать асинхронные возможности Spring с Spring Boot 2. Читайте больше статей, связанных с Spring, здесь.
Скачать исходный код
Source:
https://www.digitalocean.com/community/tutorials/spring-async-annotation