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

構築するもの

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

必要なもの

このガイドを完了する方法

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

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

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

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

Spring Initializrから開始

すべてのSpringアプリケーションの場合、Spring Initializr(英語) から開始する必要があります。Initializrは、アプリケーションに必要なすべての依存関係をすばやく取り込む方法を提供し、多くのセットアップを行います。この例では、Spring Web依存関係のみが必要です。次のイメージは、このサンプルプロジェクト用に設定されたInitializrを示しています。

initializr
前の図は、Mavenがビルドツールとして選択されたInitializrを示しています。Gradleも使用できます。また、com.example および async-method の値をそれぞれグループおよびアーティファクトとして表示します。このサンプルの残りの部分では、これらの値を使用します。

次のリストは、Mavenを選択したときに作成される pom.xml ファイルを示しています。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>async-method</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>async-method</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<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>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

次のリストは、Gradleを選択したときに作成される build.gradle ファイルを示しています。

plugins {
	id 'org.springframework.boot' version '2.2.2.RELEASE'
	id 'io.spring.dependency-management' version '1.0.8.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

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> です。このコードは、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 Initalizrは、AsyncMethodApplication クラスを作成しました。Spring Initalizr( 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 をカスタマイズします。ここでは、Springが検索する特定のメソッド名であるため、メソッドの名前は taskExecutor(Javadoc) です。今回のケースでは、同時スレッドの数を2に制限し、キューのサイズを500に制限します。チューニングできることは他にもたくさんありますExecutor Beanを定義しない場合、Springは SimpleAsyncTaskExecutor を作成して使用します。

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

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

要約

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

関連事項

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

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

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