Spring Bootの非同期処理とは?初心者向けに@Asyncの使い方を解説!
新人
「Webアプリで重い処理を実行すると、画面が固まったり遅くなることがあるんですが、どうすればいいんでしょうか?」
先輩
「それは『同期処理』のままだと、リクエスト処理の中で重たい処理が終わるまで待っているからですね。『非同期処理』を使えば解決できますよ。」
新人
「非同期処理って難しそうですが、Spring Bootで簡単に使えるんですか?」
先輩
「Spring Bootなら@Asyncというアノテーションを使うだけで、簡単に非同期処理が実現できます。順番に説明していきましょう。」
1. 非同期処理とは?
非同期処理とは、ある処理の実行を他の処理と並行して行う仕組みのことです。これにより、処理待ちの間もアプリケーション全体の応答性を保つことができます。逆に、非同期ではない「同期処理」では、処理が完了するまで次の処理に進めません。
例えば、ファイルのアップロードや外部APIとの通信、データベースの大量更新など、時間がかかる処理がある場合、非同期処理を活用するとユーザーの待ち時間を短縮できます。
Javaでは、もともとThreadクラスやExecutorServiceを使って非同期処理を記述することができますが、Spring Bootではそれを簡潔に記述できるように@Asyncという仕組みが用意されています。
2. Spring Bootにおける非同期処理の基礎
Spring Bootでは、非同期処理を有効化するために@EnableAsyncアノテーションを設定し、非同期にしたいメソッドに@Asyncアノテーションを付けるだけで、簡単に非同期処理が実装できます。
ここでは、PleiadesとGradleで構築したSpring Bootプロジェクトを前提に、@Controllerクラス内での非同期処理の実装方法を紹介します。
2-1. 依存関係の確認(Gradle)
まずは、Spring Bootプロジェクトに必要な依存関係が含まれているかを確認します。Pleiadesでプロジェクト作成時に「Spring Web」と「Spring Boot DevTools」などを追加していれば基本的に問題ありません。
2-2. 非同期処理を有効にする設定
@EnableAsyncアノテーションは、非同期処理を有効にするための設定です。通常、@SpringBootApplicationが付いているメインクラスに追記します。
package com.example.asyncdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}
2-3. 非同期で実行する処理を定義する
次に、非同期で動かしたい処理を@Asyncで定義します。例えば時間のかかる処理をサービスクラスに書くと、非同期で呼び出せるようになります。
package com.example.asyncdemo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class HeavyTaskService {
@Async
public void runHeavyTask() {
try {
Thread.sleep(5000); // 処理に5秒かかる想定
System.out.println("重い処理が完了しました");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
2-4. コントローラから非同期処理を呼び出す
最後に、@Controllerクラスから先ほど定義した非同期処理を呼び出してみましょう。リクエスト処理はすぐに完了し、非同期処理はバックグラウンドで動作します。
package com.example.asyncdemo.controller;
import com.example.asyncdemo.service.HeavyTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class AsyncController {
@Autowired
private HeavyTaskService heavyTaskService;
@GetMapping("/start-task")
@ResponseBody
public String startAsyncTask() {
heavyTaskService.runHeavyTask(); // 非同期で実行される
return "非同期タスクを開始しました!";
}
}
2-5. 実行結果の例
実際にhttp://localhost:8080/start-taskにアクセスすると、ブラウザにはすぐに次のメッセージが表示されます。
非同期タスクを開始しました!
その後、コンソールに次のようなメッセージが5秒後に表示されます。
重い処理が完了しました
2-6. 注意点:戻り値を受け取りたいときは?
@Asyncを使ったメソッドの戻り値を使いたい場合は、FutureやCompletableFutureを使って非同期の結果を受け取る設計が必要です。今回のようなvoid型では戻り値はありません。
初心者向けにはまずvoid型から始めて、慣れてきたらCompletableFutureを使った実装にもチャレンジするとよいでしょう。
3. @Asyncを使った非同期メソッドの作り方(引数あり・戻り値あり)
@Asyncは引数付きのメソッドにも対応しています。たとえば、ファイル名やユーザーIDなど、処理に必要な情報を引数として渡すことができます。
以下は、ユーザー名を引数に受け取り、処理を非同期で行う例です。
package com.example.asyncdemo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class UserTaskService {
@Async
public void processUser(String username) {
try {
Thread.sleep(3000);
System.out.println(username + " さんの処理が完了しました");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
このように、引数を受け取って処理を非同期で実行することで、柔軟に業務ロジックを構築できます。
もちろん、戻り値を持たせることも可能です。ただし、その場合はFutureやCompletableFutureといった非同期結果を保持する仕組みを使います。
4. 戻り値にFutureやCompletableFutureを使う方法
@Asyncを使った非同期処理で結果を取得したい場合、FutureやCompletableFutureを使用することで戻り値を扱うことができます。
ここでは、CompletableFutureを使った例を紹介します。非同期処理の完了後に結果を取得することができ、処理の進行を柔軟に制御できる利点があります。
package com.example.asyncdemo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class ReportService {
@Async
public CompletableFuture<String> generateReport(String userId) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "レポート生成完了: " + userId;
return CompletableFuture.completedFuture(result);
}
}
このように、非同期メソッドの戻り値としてCompletableFutureを返すと、呼び出し元で結果を後から取得することができます。コントローラ側では以下のように使用できます。
package com.example.asyncdemo.controller;
import com.example.asyncdemo.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.concurrent.CompletableFuture;
@Controller
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/report")
@ResponseBody
public String requestReport() throws Exception {
CompletableFuture<String> future = reportService.generateReport("user123");
String result = future.get(); // 処理完了を待機
return result;
}
}
上記のようにget()を呼び出すことで、非同期処理が終わるまで待つことができます。ただし、完全な非同期動作を維持するには、呼び出し元でも非同期で設計する必要があります。
5. 非同期処理とスレッドプールのカスタマイズ
@Asyncを使った非同期処理では、実際には内部でスレッドが動いています。そのスレッドを管理しているのが「スレッドプール」です。Spring Bootでは、デフォルトのスレッドプールが用意されていますが、同時実行数やキューサイズなどを調整したい場合はカスタマイズが可能です。
以下のようにAsyncConfigurerインターフェースを実装することで、スレッドプールの設定を変更できます。
package com.example.asyncdemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 最小スレッド数
executor.setMaxPoolSize(10); // 最大スレッド数
executor.setQueueCapacity(25); // キューの最大長
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}
上記の設定をすることで、同時に実行される非同期処理の数を制限でき、過負荷による性能低下を防ぐことができます。特にアクセス数が多いWebアプリでは、スレッドプールの調整は重要なチューニングポイントです。
また、スレッドの名前にプレフィックスを付けることで、ログ出力などで非同期処理のトレースがしやすくなります。
なお、スレッドプールのサイズは実行環境や処理内容に応じて適切に設定してください。設定が小さすぎると処理が滞り、大きすぎるとメモリを圧迫する可能性があります。
6. 非同期処理における注意点(例外処理、トランザクションの影響)
@Asyncを使った非同期処理では、通常の同期処理と異なる動作になる点に注意が必要です。特に重要なのは「例外処理」と「トランザクションの挙動」です。
まず例外処理ですが、@Asyncで実行されたメソッド内で例外が発生しても、呼び出し元には通知されません。非同期メソッドの戻り値がvoidの場合、例外が発生してもログに出力されるだけで、アプリケーションのフローには影響しないように見えることがあります。
これを防ぐためには、AsyncUncaughtExceptionHandlerを設定することで、非同期処理で発生した未処理の例外をキャッチできます。
package com.example.asyncdemo.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
@Configuration
public class AsyncErrorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return null; // 既存設定と併用する場合は省略
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, obj) -> {
System.err.println("非同期処理で例外発生: " + throwable.getMessage());
System.err.println("メソッド: " + method.getName());
};
}
}
このように設定すると、バックグラウンドで発生した例外の原因を把握しやすくなります。
次にトランザクションとの関係についてです。非同期メソッドは、呼び出し元のトランザクションスコープから独立して動作します。そのため、@Transactionalが非同期メソッド内にあっても、思ったようにロールバックされないケースがあります。
もし非同期処理内でトランザクションを使いたい場合は、非同期メソッド自体を別のクラスに分けて@Transactionalを付与することが推奨されます。
7. 非同期処理の停止と監視(shutdownやログ活用)
非同期処理を運用する上で大切なのが「適切な停止」と「ログ監視」です。アプリケーション終了時にバックグラウンドスレッドが生き残っていると、プロセスが完全に終了しないことがあります。
この問題を避けるためには、ThreadPoolTaskExecutorのshutdown処理を正しく呼び出すことが重要です。
package com.example.asyncdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executor;
@Configuration
public class GracefulShutdownConfig {
private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@Bean("taskExecutor")
public Executor taskExecutor() {
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("ShutdownAware-");
executor.initialize();
return executor;
}
@PreDestroy
public void onShutdown() {
executor.shutdown();
System.out.println("非同期スレッドをシャットダウンしました");
}
}
このように@PreDestroyを使うことで、アプリケーション終了時に非同期処理を安全に停止できます。
また、非同期処理では進捗や失敗を追いやすくするためにログ出力が重要です。System.out.println()でも構いませんが、本番環境ではLoggerを使うとより便利です。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);
@Async
public void processData() {
logger.info("非同期処理を開始します");
// 処理内容
logger.info("非同期処理が完了しました");
}
ログレベルを調整することで、開発中は詳細に、運用中は簡潔に出力するなど柔軟に管理できます。
8. 初心者が非同期処理を学ぶための学習ステップとおすすめ教材
Spring Bootでの非同期処理は奥が深く、最初は戸惑うことも多いですが、段階的に学んでいけば確実に理解が深まります。ここでは初心者向けにおすすめの学習ステップを紹介します。
ステップ1:基本概念の理解
まずは「同期」と「非同期」の違いをしっかり理解しましょう。処理の流れがどう変わるのか、図を使って学ぶのがおすすめです。
ステップ2:@Asyncを使って実際に試す
簡単なThread.sleep()を使った非同期メソッドを作って、動作の違いを確認しましょう。動作確認はSystem.out.printlnで十分です。
ステップ3:戻り値・例外処理・スレッドプールを学ぶ
非同期処理の設計で重要なのがCompletableFutureやスレッド管理です。これらを学ぶことで、実践的な設計が可能になります。
ステップ4:公式ドキュメントや実践書籍を読む
Spring公式ドキュメントの「Spring Task Execution and Scheduling」セクションはとても参考になります。日本語書籍では「Spring徹底入門」なども非同期処理をカバーしています。
ステップ5:実際のアプリで使ってみる
開発中のプロジェクトでファイルアップロード処理や集計処理など、時間のかかる処理を@Asyncに置き換えてみましょう。効果が実感しやすく、理解も進みます。
非同期処理は、Spring Bootアプリのレスポンス性能を高めるための重要な技術です。焦らず、段階的に学びながら、自分のアプリに取り入れてみてください。