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

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

構築するもの

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

必要なもの

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

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

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

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

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

Spring Initializr から開始

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

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

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

  2. Gradle または Maven のいずれかと、使用する言語を選択してください。

  3. 依存関係をクリックし、Spring Web と HTTP クライアントを選択します。

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

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

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

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

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

ユーザー表現をモデル化するには、次の例に示すように、不変のリソース表現クラス(Java レコードまたは Kotlin データクラス)を作成します。

Java
package com.example.asyncmethod;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record User(String name, String blog) {
}
Kotlin
package com.example.asyncmethod

import com.fasterxml.jackson.annotation.JsonIgnoreProperties

@JsonIgnoreProperties(ignoreUnknown = true)
data class User(val name: String?, val blog: String?)

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

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

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

次に、GitHub にクエリを実行してユーザー情報を取得するサービスを作成する必要があります。以下のリストはその方法を示しています。

Java
package com.example.asyncmethod;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.concurrent.CompletableFuture;

@Service
public class GitHubLookupService {

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

	private final RestClient restClient;

	public GitHubLookupService(RestClient.Builder restClientBuilder) {
		this.restClient = restClientBuilder.build();
	}

	@Async
	public CompletableFuture<User> findUser(String user) throws InterruptedException {
		logger.info("Looking up " + user);
		User results = restClient.get()
				.uri("https://api.github.com/users/{user}", user)
				.retrieve()
				.body(User.class);
		// Artificial delay of 1s for demonstration purposes
		Thread.sleep(1000L);
		return CompletableFuture.completedFuture(results);
	}
}
Kotlin
package com.example.asyncmethod

import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import org.springframework.web.client.requiredBody
import java.util.concurrent.CompletableFuture

@Service
class GitHubLookupService(restClientBuilder: RestClient.Builder) {

    private val logger = LoggerFactory.getLogger(javaClass)

    private val restClient = restClientBuilder.build()

    @Async
    fun findUser(user: String): CompletableFuture<User> {
        logger.info("Looking up $user")
        val results = restClient.get()
            .uri("https://api.github.com/users/{user}", user)
            .retrieve()
            .requiredBody<User>()
        // Artificial delay of 1s for demonstration purposes
        Thread.sleep(1000L)
        return CompletableFuture.completedFuture(results)
    }
}

GitHubLookupService クラスは、Spring の RestClient を使用して リモート REST ポイント (api.github.com/users/) を呼び出し、その応答を User オブジェクトに変換します。Spring Boot は、spring-boot-starter-restclient の依存関係がクラスパスにある場合、自動構成ビット (つまり HttpMessageConverter) を使用してデフォルトをカスタマイズする RestClient.Builder を自動的に提供します。

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

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

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

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

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

サンプルを実行するには、実行可能ファイル jar を作成します。Spring の @Async アノテーションは Web アプリケーションで動作しますが、その利点を確認するために Web コンテナーを設定する必要はありません。以下にその方法を示します。

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;
	}
}
Kotlin
package com.example.asyncmethod

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
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
class AsyncMethodApplication {

    @Bean
    fun taskExecutor(): Executor = ThreadPoolTaskExecutor().apply {
        corePoolSize = 2
        maxPoolSize = 2
        queueCapacity = 500
        setThreadNamePrefix("GithubLookup-")
    }
}

fun main(args: Array<String>) {
    // close the application context to shut down the custom ExecutorService
    runApplication<AsyncMethodApplication>(*args).close()
}
Spring Initializr は、AsyncMethodApplication クラスを自動的に作成しました。このクラスは、Spring Initializr からダウンロードした 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 もあります。

アプリケーションを実行するには、以下のリストに示すようなクラスも必要です。

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 off 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());

	}
}
Kotlin
package com.example.asyncmethod

import org.slf4j.LoggerFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component
import java.util.concurrent.CompletableFuture

@Component
class AppRunner(private val gitHubLookupService: GitHubLookupService) : CommandLineRunner {

    private val logger = LoggerFactory.getLogger(javaClass)

    override fun run(vararg args: String) {
        // Start the clock
        val start = System.currentTimeMillis()

        // Kick off multiple, asynchronous lookups
        val page1 = gitHubLookupService.findUser("PivotalSoftware")
        val page2 = gitHubLookupService.findUser("CloudFoundry")
        val 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.0.1-SNAPSHOT.jar

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

java -jar target/gs-async-method-0.0.1-SNAPSHOT.jar

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

以下のリストは、このサンプルアプリケーションからの典型的な出力例(Java の User レコードを使用)を示しています。

2026-04-09T14:50:49.565+02:00  INFO 94338 --- [ GithubLookup-2] c.e.asyncmethod.GitHubLookupService      : Looking up CloudFoundry
2026-04-09T14:50:49.565+02:00  INFO 94338 --- [ GithubLookup-1] c.e.asyncmethod.GitHubLookupService      : Looking up PivotalSoftware
2026-04-09T14:50:50.876+02:00  INFO 94338 --- [ GithubLookup-2] c.e.asyncmethod.GitHubLookupService      : Looking up Spring-Projects
2026-04-09T14:50:52.067+02:00  INFO 94338 --- [           main] com.example.asyncmethod.AppRunner        : Elapsed time: 2504
2026-04-09T14:50:52.068+02:00  INFO 94338 --- [           main] com.example.asyncmethod.AppRunner        : --> User[name=Pivotal Software, Inc., blog=http://pivotal.io]
2026-04-09T14:50:52.068+02:00  INFO 94338 --- [           main] com.example.asyncmethod.AppRunner        : --> User[name=Cloud Foundry, blog=https://www.cloudfoundry.org/]
2026-04-09T14:50:52.069+02:00  INFO 94338 --- [           main] com.example.asyncmethod.AppRunner        : --> User[name=Spring, blog=https://spring.io/projects]

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

基本的に、タスクの実行時間が長くなるほど、また同時に呼び出されるタスクの数が増えるほど、非同期処理によるメリットは大きくなります。ただし、CompletableFuture クラスの処理というトレードオフが生じます。これは、結果を直接扱うことができなくなるため、間接的な処理レイヤーが追加されることを意味します。

要約

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

関連事項

次のガイドも役立つかもしれません:

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

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

コードを入手する