@Async アノテーションで非同期メソッドの作成

このガイドでは、GitHub への非同期クエリを作成する方法を説明します。焦点は非同期部分にあります。これは、サービスをスケーリングするときによく使用される機能です。

構築するもの

GitHub ユーザー情報を照会し、GitHub の API を介してデータを取得する検索サービスを構築します。サービスをスケーリングする 1 つの方法は、バックグラウンドで高負荷なジョブを実行し、Java の CompletableFuture (標準 Javadoc) インターフェースを使用して結果を待つことです。Java の CompletableFuture は、通常の Future から進化したものです。複数の非同期操作をパイプライン化し、単一の非同期計算にマージすることが簡単になります。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-async-method/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを手動で初期化するには:

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択します。このガイドは、Java を選択したことを前提としています。

  3. 依存関係をクリックして、Spring Web を選択します。

  4. 生成をクリックします。

  5. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

EclipseIntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。
プロジェクトを Github からフォークして、IDE または他のエディターで開くこともできます。

GitHub ユーザーの表現を作成する

GitHub ルックアップサービスを作成する前に、GitHub の API を通じて取得するデータの表現を定義する必要があります。

ユーザー表現をモデル化するには、リソース表現クラスを作成します。これを行うには、次の例(src/main/java/com/example/asyncmethod/User.java から)が示すように、フィールド、コンストラクター、アクセサーを持つプレーンな古い Java オブジェクトを提供します。

package com.example.asyncmethod;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true)
public class User {

  private String name;
  private String blog;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getBlog() {
    return blog;
  }

  public void setBlog(String blog) {
    this.blog = blog;
  }

  @Override
  public String toString() {
    return "User [name=" + name + ", blog=" + blog + "]";
  }

}

Spring は Jackson JSON (英語) ライブラリを使用して、GitHub の JSON レスポンスを User オブジェクトに変換します。@JsonIgnoreProperties アノテーションは、クラスにリストされていない属性を無視するよう Spring に指示します。これにより、REST 呼び出しとドメインオブジェクトの生成が簡単になります。

このガイドでは、デモンストレーション用に name および blog URL のみを取得します。

GitHub ルックアップサービスを作成する

次に、GitHub を照会してユーザー情報を検索するサービスを作成する必要があります。次のリスト(src/main/java/com/example/asyncmethod/GitHubLookupService.java から)は、その方法を示しています。

package com.example.asyncmethod;

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 GitHubLookupService {

  private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);

  private final RestTemplate restTemplate;

  public GitHubLookupService(RestTemplateBuilder restTemplateBuilder) {
    this.restTemplate = restTemplateBuilder.build();
  }

  @Async
  public CompletableFuture<User> findUser(String user) throws InterruptedException {
    logger.info("Looking up " + user);
    String url = String.format("https://api.github.com/users/%s", user);
    User results = restTemplate.getForObject(url, User.class);
    // Artificial delay of 1s for demonstration purposes
    Thread.sleep(1000L);
    return CompletableFuture.completedFuture(results);
  }

}

GitHubLookupService クラスは Spring の RestTemplate を使用してリモート REST ポイント(api.github.com/users/)を呼び出してから、回答を User オブジェクトに変換します。Spring Boot は、自動構成ビット(つまり MessageConverter)を使用してデフォルトをカスタマイズする RestTemplateBuilder を自動的に提供します。

クラスには @Service アノテーションが付けられており、Spring のコンポーネントスキャンの候補となり、アプリケーションコンテキストを検出して追加します。

findUser メソッドには Spring の @Async アノテーションが付いており、別のスレッドで実行する必要があることを示しています。メソッドの戻り値の型は、非同期サービスの要件である User ではなく CompletableFuture<User> (標準 Javadoc) です。このコードは、completedFuture メソッドを使用して、GitHub クエリの結果ですでに完了している CompletableFuture インスタンスを返します。

GitHubLookupService クラスのローカルインスタンスを作成しても、findUser メソッドは非同期に実行できません。@Configuration クラス内で作成するか、@ComponentScan で取得する必要があります。

GitHub の API のタイミングはさまざまです。このガイドの後半で利点を示すために、このサービスには 1 秒の遅延が追加されています。

アプリケーションを実行可能にする

サンプルを実行するには、実行可能な jar を作成できます。Spring の @Async アノテーションは Web アプリケーションで機能しますが、その利点を確認するために Web コンテナーをセットアップする必要はありません。次のリスト(src/main/java/com/example/asyncmethod/AsyncMethodApplication.java から)は、その方法を示しています。

package com.example.asyncmethod;

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 AsyncMethodApplication {

  public static void main(String[] args) {
    // close the application context to shut down the custom ExecutorService
    SpringApplication.run(AsyncMethodApplication.class, args).close();
  }

  @Bean
  public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(2);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("GithubLookup-");
    executor.initialize();
    return executor;
  }


}
Spring Initializr は、AsyncMethodApplication クラスを作成しました。これは、Spring Initializr(src/main/java/com/example/asyncmethod/AsyncMethodApplication.java 内)からダウンロードした zip ファイルにあります。そのクラスをプロジェクトにコピーしてから変更するか、前のリストからクラスをコピーすることができます。

@SpringBootApplication は、次のすべてを追加する便利なアノテーションです。

  • @Configuration: アプリケーションコンテキストの Bean 定義のソースとしてクラスにタグを付けます。

  • @EnableAutoConfiguration: クラスパス設定、他の Bean、さまざまなプロパティ設定に基づいて Bean の追加を開始するよう Spring Boot に指示します。例: spring-webmvc がクラスパスにある場合、このアノテーションはアプリケーションに Web アプリケーションとしてフラグを立て、DispatcherServlet のセットアップなどの主要な動作をアクティブにします。

  • @ComponentScan: Spring に、com/example パッケージ内の他のコンポーネント、構成、サービスを探して、コントローラーを検出させるように指示します。

main() メソッドは、Spring Boot の SpringApplication.run() メソッドを使用してアプリケーションを起動します。XML が 1 行もないことに気付きましたか? web.xml ファイルもありません。この Web アプリケーションは 100% 純粋な Java であり、接続機能やインフラストラクチャの構成に対処する必要はありませんでした。

@EnableAsync アノテーションは、バックグラウンドスレッドプールで @Async メソッドを実行する Spring の機能をオンにします。このクラスは、新しい Bean を定義することによって Executor もカスタマイズします。ここでは、メソッドの名前は taskExecutor です。これは、Spring が検索する特定のメソッド名 (Javadoc) です。この例では、同時スレッドの数を 2 に制限し、キューのサイズを 500 に制限します。調整できる項目は他にもたくさんあります。Executor Bean を定義しない場合、Spring は ThreadPoolTaskExecutor を使用します。

GitHubLookupService を挿入し、そのサービスを 3 回呼び出して、メソッドが非同期に実行されることを示す CommandLineRunner もあります。

また、アプリケーションを実行するにはクラスが必要です。src/main/java/com/example/asyncmethod/AppRunner.java で見つけることができます。次のリストは、そのクラスを示しています。

package com.example.asyncmethod;

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 AppRunner implements CommandLineRunner {

  private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

  private final GitHubLookupService gitHubLookupService;

  public AppRunner(GitHubLookupService gitHubLookupService) {
    this.gitHubLookupService = gitHubLookupService;
  }

  @Override
  public void run(String... args) throws Exception {
    // Start the clock
    long start = System.currentTimeMillis();

    // Kick of multiple, asynchronous lookups
    CompletableFuture<User> page1 = gitHubLookupService.findUser("PivotalSoftware");
    CompletableFuture<User> page2 = gitHubLookupService.findUser("CloudFoundry");
    CompletableFuture<User> page3 = gitHubLookupService.findUser("Spring-Projects");

    // Wait until they are all done
    CompletableFuture.allOf(page1,page2,page3).join();

    // Print results, including elapsed time
    logger.info("Elapsed time: " + (System.currentTimeMillis() - start));
    logger.info("--> " + page1.get());
    logger.info("--> " + page2.get());
    logger.info("--> " + page3.get());

  }

}

実行可能 JAR を構築する

コマンドラインから Gradle または Maven を使用してアプリケーションを実行できます。必要なすべての依存関係、クラス、リソースを含む単一の実行可能 JAR ファイルを構築して実行することもできます。実行可能な jar を構築すると、開発ライフサイクル全体、さまざまな環境などで、アプリケーションとしてサービスを簡単に提供、バージョン管理、デプロイできます。

Gradle を使用する場合、./gradlew bootRun を使用してアプリケーションを実行できます。または、次のように、./gradlew build を使用して JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar build/libs/gs-async-method-0.1.0.jar

Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar target/gs-async-method-0.1.0.jar
ここで説明する手順は、実行可能な JAR を作成します。クラシック WAR ファイルを作成することもできます。

アプリケーションは、GitHub への各クエリを示すロギング出力を表示します。allOf ファクトリメソッドを使用して、CompletableFuture オブジェクトの配列を作成します。join メソッドを呼び出すことにより、すべての CompletableFuture オブジェクトの完了を待つことができます。

次のリストは、このサンプルアプリケーションからの典型的な出力を示しています。

2016-09-01 10:25:21.295  INFO 17893 --- [ GithubLookup-2] hello.GitHubLookupService                : Looking up CloudFoundry
2016-09-01 10:25:21.295  INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService                : Looking up PivotalSoftware
2016-09-01 10:25:23.142  INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService                : Looking up Spring-Projects
2016-09-01 10:25:24.281  INFO 17893 --- [           main] hello.AppRunner                          : Elapsed time: 2994
2016-09-01 10:25:24.282  INFO 17893 --- [           main] hello.AppRunner                          : --> User [name=Pivotal Software, Inc., blog=https://pivotal.io]
2016-09-01 10:25:24.282  INFO 17893 --- [           main] hello.AppRunner                          : --> User [name=Cloud Foundry, blog=https://www.cloudfoundry.org/]
2016-09-01 10:25:24.282  INFO 17893 --- [           main] hello.AppRunner                          : --> User [name=Spring, blog=https://spring.io/projects]

最初の 2 つの呼び出しは別々のスレッド(GithubLookup-2GithubLookup-1)で発生し、3 番目の呼び出しは 2 つのスレッドの 1 つが使用可能になるまで保留されることに注意してください。非同期機能なしでこれにかかる時間を比較するには、@Async アノテーションをコメントアウトして、サービスを再実行してみてください。各クエリには少なくとも 1 秒かかるため、合計経過時間は著しく増加するはずです。たとえば、Executor を調整して、corePoolSize 属性を増やすこともできます。

基本的に、タスクにかかる時間が長くなり、同時に呼び出されるタスクが増えるほど、非同期にすることで得られるメリットが大きくなります。トレードオフは、CompletableFuture インターフェースを処理することです。結果を直接処理しなくなるため、間接的なレイヤーが追加されます。

要約

おめでとう! 複数の呼び出しを一度にスケーリングできる非同期サービスを開発しました。

関連事項

次のガイドも役立ちます。

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の Attribution、NoDerivatives creative commons ライセンス (英語) でリリースされています。

コードを入手する