Поиск по сайту:

Spring @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
<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

Включение асинхронной поддержки

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

    // standard getters and setters

    @Override
    public String toString() {
        return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
    }
}

Мы использовали @JsonIgnoreProperties, чтобы, если в ответе было больше атрибутов, Spring мог их безопасно игнорировать.

Создание службы

Пришло время определить нашу службу, которая будет вызывать упомянутый Movie 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);
        // Artificial delay of 1s for demonstration purposes
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

Этот класс является @Service, что делает его подходящим для Spring Component Scan. Метод lookForMovie возвращает тип CompletableFuture, что является требованием для любой асинхронной службы. Поскольку время для API может варьироваться, мы добавили задержку в 2 секунды для демонстрации.

Создание запускателя командной строки

Мы будем запускать наше приложение с помощью CommandLineRunner, что является самым простым способом тестирования нашего приложения. CommandLineRunner запускается сразу после инициализации всех bean-компонентов приложения. Давайте посмотрим код для 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 {
        // Start the clock
        long start = System.currentTimeMillis();

        // Kick of multiple, asynchronous lookups
        CompletableFuture<MovieModel> page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
        CompletableFuture<MovieModel> page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
        CompletableFuture<MovieModel> page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");

        // Join all threads so that we can wait until all are done
        CompletableFuture.allOf(page1, page2, page3).join();

        // Print results, including elapsed time
        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 читайте здесь.

Загрузите исходный код

Скачать пример проекта Spring Boot Async