package com.example.asyncmethod;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public record User(String name, String blog) {
}@Async アノテーションで非同期メソッドの作成
このガイドでは、GitHub への非同期クエリを作成する方法を説明します。焦点は非同期部分にあります。これは、サービスをスケーリングするときによく使用される機能です。
構築するもの
GitHub のユーザー情報を照会し、GitHub の API を介してデータを取得するルックアップサービスを構築します。サービスのスケーリングアプローチの 1 つは、CompletableFuture (標準 Javadoc) クラスを使用して、バックグラウンドで負荷の高いジョブを実行し、結果を待つことです。CompletableFuture は通常の Future の進化形です。複数の非同期操作を簡単にパイプライン化し、単一の非同期計算にマージできます。
必要なもの
約 15 分
Eclipse STS や IntelliJ IDEA のような任意の IDE または VSCode のようなテキストエディター
Java 17 以降
コードを直接 IDE にインポートすることもできます。
本ガイドの完成までの流れ
ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。
最初から始めるには、Spring Initializr から開始に進みます。
基本をスキップするには、次の手順を実行します。
このガイドのソースリポジトリをダウンロードして解凍するか、Git を使用してクローンを作成してください:
git clone https://github.com/spring-guides/gs-async-method.gitgs-async-method/initialに cdGitHub ユーザーの表現を作成するにジャンプしてください。
完了したときは、gs-async-method/complete のコードに対して結果を確認できます。
Spring Initializr から開始
IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。
プロジェクトを手動で初期化するには:
IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。
Gradle または Maven のいずれかと、使用する言語を選択してください。
依存関係をクリックし、Spring Web と HTTP クライアントを選択します。
生成をクリックします。
結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。
| Eclipse や IntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。 |
| プロジェクトを GitHub からフォークして、IDE または他のエディターで開くこともできます。 |
GitHub ユーザーの表現を作成する
GitHub ルックアップサービスを作成する前に、GitHub の API を通じて取得するデータの表現を定義する必要があります。
ユーザー表現をモデル化するには、次の例に示すように、不変のリソース表現クラス(Java レコードまたは 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 にクエリを実行してユーザー情報を取得するサービスを作成する必要があります。以下のリストはその方法を示しています。
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);
}
}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)
}
}The GitHubLookupService class uses Spring ’ s RestClient to invoke a remote REST point (api.github.com/users/) and then convert the answer into a User object. Spring Boot automatically provides a RestClient.Builder that customizes the defaults with any auto-configuration bits (that is, HttpMessageConverter), provided that the spring-boot-starter-restclient dependency is on the classpath.
クラスには @Service アノテーションが付けられており、Spring のコンポーネントスキャンの候補となり、アプリケーションコンテキストを検出して追加します。
The findUser method is flagged with Spring ’ s @Async annotation, indicating that it should run on a separate thread. The method ’ s return type is CompletableFuture<User> (標準 Javadoc) instead of User, a requirement for any asynchronous service. This code uses the completedFuture method to return a CompletableFuture instance that is already completed with the result of the GitHub query.
GitHubLookupService クラスのローカルインスタンスを作成しても、findUser メソッドは非同期に実行できません。@Configuration クラス内で作成するか、@ComponentScan で取得する必要があります。 |
GitHub の API のタイミングはさまざまです。このガイドの後半で利点を示すために、このサービスには 1 秒の遅延が追加されています。
アプリケーションを実行可能にする
To run a sample, you can create an executable jar. Spring ’ s @Async annotation works with web applications, but you need not set up a web container to see its benefits. The following listing shows how to do so:
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;
}
}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()
}The Spring Initializr created an AsyncMethodApplication class for you. You can find it in the zip file that you downloaded from the Spring Initializr. You can either copy that class to your project and then modify it or copy the class from the preceding listing. |
@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 もあります。
You also need a class to run the application as the following listing shows:
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());
}
}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 ファイルを実行できます。
Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。
アプリケーションは、GitHub への各クエリを示すロギング出力を表示します。allOf ファクトリメソッドを使用して、CompletableFuture オブジェクトの配列を作成します。join メソッドを呼び出すことにより、すべての CompletableFuture オブジェクトの完了を待つことができます。
The following listing shows typical output (using the Java User record) from this sample application:
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]
Note that the first two calls happen in separate threads (GithubLookup-2, GithubLookup-1) and the third one is parked until one of the two threads became available. To compare how long this takes without the asynchronous feature, try commenting out the @Async annotation and running the service again. The total elapsed time should increase noticeably, because each query takes at least a second. You can also tune the Executor to increase the corePoolSize attribute for instance.
Essentially, the longer the task takes and the more tasks are invoked simultaneously, the more benefit you see from making things asynchronous. The trade-off is handling the CompletableFuture class. It adds a layer of indirection, because you are no longer dealing directly with the results.
要約
おめでとう! 複数の呼び出しを一度にスケーリングできる非同期サービスを開発しました。
関連事項
次のガイドも役立つかもしれません:
新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語) 。
| すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。 |