非同期処理のための Spring @Async アノテーション

Springの@Asyncアノテーションは、Springで非同期メソッドを作成することができます。このチュートリアルでは、Springフレームワークで@Asyncを探ってみましょう。簡単に説明すると、@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を実行する場合、生成コマンドを完了するまで数秒かかります。Mavenは生成タスクを実行するために必要なすべてのプラグインとアーティファクトをダウンロードする必要があります。プロジェクト作成の様子は次のようになります: Mavenを使用したプロジェクトの作成プロジェクトを作成したら、お気に入りのIDEで開いてください。次に、プロジェクトに適切な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

このコマンドを実行すると、次のような依存関係のツリーが表示されます:

非同期サポートの有効化

Asyncサポートを有効にするには、単一のアノテーションを使用するだけです。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;

    // 標準のゲッターとセッター

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

Springが応答にさらに属性がある場合、@JsonIgnorePropertiesを使用して安全に無視できるようにしました。

サービスの作成

それでは、言及されたMovie APIを呼び出すためのServiceを定義する時が来ました。私たちは、単純な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秒の遅延を追加しました。

CommandLineRunnerの作成

アプリケーションをテストする最も簡単な方法である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 {
        // 時間計測の開始
        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());
    }
}

私たちはただRestTemplateを使用して、いくつかのランダムに選ばれた映画IDを持つサンプル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-1JDAsync-2の2つだけです。

結論

このレッスンでは、Spring Boot 2を使用したSpringの非同期機能の使い方について学びました。Springに関連する記事はこちらをご覧ください。

ソースコードのダウンロード

Spring Boot Asyncのサンプルプロジェクトをダウンロード

Source:
https://www.digitalocean.com/community/tutorials/spring-async-annotation