概要
- サンプルプログラムを書いて Spring Boot + Spring Retry の挙動を確認する
- Spring Retry を使うと処理の再試行を判断・制御するコードを書く必要がなくなる
- 試行する回数やインターバル時間は @Retryable アノテーションに記述する
サンプルプログラムのソースコード一覧
├── build.gradle
├── settings.gradle
└── src
└── main
└── java
└── info
└── maigo
└── lab
└── sample
└── springretry
├── Application.java
├── HogeController.java
├── HogeService.java
├── KotsuException.java
├── PonException.java
├── PonKotsuRepository.java
└── PonKotsuRetryListener.java
サンプルプログラム概要
- HTTPリクエストに対してJSONでメッセージを返す。
- PonKotsuRepository はポンコツなリポジトリ。一定の確率でメッセージを返すがほとんどは失敗する。
- HogeService は PonKotsuRepository が失敗したら再度 PonKotsuRepository を呼び出してリトライする。
- 試行する回数やインターバル時間は @Retryable アノテーションに記述する。
- 一定回数失敗したらあきらめる。
- PonKotsuRepository は失敗の際に PonException または KotsuException を返す。
- 最後の失敗が PonException の場合は、 Spring Retry の @Recover アノテーションを指定したメソッドで処理する。
- 最後の失敗が KotsuException の場合は、 Spring Retry の @ExceptionHandler アノテーションを指定したメソッドで処理する。
- RetryListener インターフェースを実装したクラスで、リトライ処理の状況を標準出力に出力する。
動作確認環境
- macOS Mojave
- OpenJDK 11.0.2
- spring-boot-starter-web:2.2.0.M4
- spring-retry:1.2.4.RELEASE
Gradle のビルド用ファイル
build.gradle
Spring Retry を使用するため dependencies に implementation 'org.springframework.retry:spring-retry:1.2.4.RELEASE' を追加する。
また、実行時に AOP クラスが必要なため runtime 'org.springframework.boot:spring-boot-starter-aop' を追加する。
依存ライブラリの指定方法は GitHub - spring-projects/spring-retry に載っている。
plugins {
// The Java Plugin
// https://docs.gradle.org/current/userguide/java_plugin.html
id 'java'
// Gradle - Plugin: org.springframework.boot
// https://plugins.gradle.org/plugin/org.springframework.boot
id 'org.springframework.boot' version '2.2.0.M4'
// Gradle - Plugin: io.spring.dependency-management
/// https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
}
group = 'info.maigo.lab'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/snapshot' }
maven { url 'https://repo.spring.io/milestone' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.2.0.M4'
implementation 'org.springframework.retry:spring-retry:1.2.4.RELEASE'
runtime 'org.springframework.boot:spring-boot-starter-aop'
}
settings.gradle
pluginManagement {
repositories {
maven { url 'https://repo.spring.io/snapshot' }
maven { url 'https://repo.spring.io/milestone' }
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == 'org.springframework.boot') {
useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = 'sample.springretry'
ビルドとサーバ起動
Gradle の Java プラグインにある build タスクで JAR ファイルを生成する。
$ gradle build
BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed
生成された jar ファイルを java コマンドで実行する。これで Spring Boot による Web サーバが起動する。
$ java -jar build/libs/sample.springretry-0.0.1-SNAPSHOT.jar
ソースコード
Application.java
Spring Boot による起動エントリクラス。
Spring Retry を使用するため @EnableRetry アノテーションを指定している。
package info.maigo.lab.sample.springretry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
HogeController.java
HTTPリクエストを受け取ってレスポンスを返す Controller クラス。
例外が発生した際は @ExceptionHandler アノテーションを指定したメソッドにて処理した結果を返す。
package info.maigo.lab.sample.springretry;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HogeController {
@Autowired
private HogeService hogeService;
@RequestMapping("/{message}")
public Map<String, Object> index(@PathVariable("message") String message) {
return hogeService.send(message);
}
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(HttpServletRequest req, Exception e) {
return new HashMap<String, Object>() {
{
put("handleException", e.getMessage());
}
};
}
}
PonKotsuRepository.java
ポンコツな Repository クラス。
一定の確率で PonException や KotsuException を発生させる。
package info.maigo.lab.sample.springretry;
import java.util.Random;
import org.springframework.stereotype.Repository;
/**
* ポンコツなリポジトリ。
*/
@Repository
public class PonKotsuRepository {
private static final Random r = new Random(System.currentTimeMillis());
/**
* うまくいけばメッセージを返すが、ほとんど失敗する。
* @param message
* @return メッセージ
* @throws PonException ポン例外
* @throws KotsuException コツ例外
*/
public String send(String message) {
int i = r.nextInt(5);
if (i < 2) {
System.out.println("PonKotsuRepository: Throws PonException.");
throw new PonException();
} else if (i < 4) {
System.out.println("PonKotsuRepository: Throws KotsuException.");
throw new KotsuException();
} else {
System.out.println("PonKotsuRepository: Returns a message.");
return "1/3も伝わらないメッセージ: " + message;
}
}
}
HogeService.java
ポンコツな Repository クラスを利用する Service クラス。
@Retryable アノテーションを使用して、リトライ回数、リトライまでの待ち時間を指定している。
@Recover アノテーションを指定したメソッドは、指定回数試行しても例外が発生してしまった場合にコールされる。このメソッドは、最後の試行にて発生した例外を第1引数とし、 @Retryable アノテーションを付けたメソッドの引数を第2引数以降に取る。
今回は @Recover アノテーションを指定したメソッドを2つ用意している。ひとつは PonException が発生したときに使うもので、結果を文字列で返す。もうひとつは KotsuException が発生したときに使うもので、RuntimeException を投げる。
Spring Retry で使うアノテーションの情報は Spring-Retry - シンプルで本質的なコードを汚さないリトライ処理フレームワーク や org.springframework.retry.annotation (Spring Retry 1.2.4.RELEASE API) が参考になる。
package info.maigo.lab.sample.springretry;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.retry.annotation.Recover;
@Service
public class HogeService {
@Autowired
private PonKotsuRepository repository;
@Retryable(value = {PonException.class, KotsuException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public Map<String, Object> send(String message) {
String result = repository.send(message);
return new HashMap<String, Object>() {
{
put("result", result);
}
};
}
@Recover
public Map<String, Object> recoverSend(PonException e, String message) {
System.out.println("recoverSend: PonException");
return new HashMap<String, Object>() {
{
put("error", "最後に発生したのは PonException");
}
};
}
@Recover
public Map<String, Object> recoverSend(KotsuException e, String message) {
System.out.println("recoverSend: KotsuException");
throw new RuntimeException("最後に発生したのは KotsuException");
}
}
PonException.java
ポン例外。RuntimeException を継承しているだけ。
package info.maigo.lab.sample.springretry;
public class PonException extends RuntimeException {
}
KotsuException.java
コツ例外。RuntimeException を継承しているだけ。
package info.maigo.lab.sample.springretry;
public class KotsuException extends RuntimeException {
}
PonKotsuRetryListener.java
RetryListener インターフェースを実装したクラス。
リトライ処理中に呼び出されるコールバックで構成されている。
今回の実装ではリトライ処理の状況を標準出力に出力している。
package info.maigo.lab.sample.springretry;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.stereotype.Component;
@Component
public class PonKotsuRetryListener extends RetryListenerSupport {
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("PonKotsuRetryListener#close: " + getThrowableString(throwable));
super.close(context, callback, throwable);
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("PonKotsuRetryListener#onError: " + getThrowableString(throwable));
super.onError(context, callback, throwable);
}
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
System.out.println("PonKotsuRetryListener#open");
return super.open(context, callback);
}
private static String getThrowableString(Throwable throwable) {
return throwable == null ? "null" : throwable.getClass().getSimpleName();
}
}
実行例
実行例1
curl で起動したサーバにアクセスすると JSON が出力される。
$ curl http://localhost:8080/hello
{"result":"1/3も伝わらないメッセージ: hello"}
サーバ側の標準出力を見る。
Pon例外発生後にリトライして、正常にメッセージを返すことができている。
PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Returns a message.
PonKotsuRetryListener#close: null
実行例2
$ curl http://localhost:8080/foo
{"error":"最後に発生したのは PonException"}
サーバ側の標準出力を見る。
Pon例外 → Kotsu例外 → Pon例外の順番で発生している。
最後のPon例外については PonKotsuRepository#recoverSend にて結果を返している。
PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
recoverSend: PonException
PonKotsuRetryListener#close: PonException
実行例3
$ curl http://localhost:8080/bar
{"handleException":"最後に発生したのは KotsuException"}
サーバ側の標準出力を見る。
Pon例外 → Kotsu例外 → Kotsu例外の順番で発生している。
最後のKotsu例外については HogeController#handleException で結果を返している。
PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
recoverSend: KotsuException
PonKotsuRetryListener#close: KotsuException
参考資料
- GitHub - spring-projects/spring-retry
- org.springframework.retry:spring-retry:1.2.4.RELEASE API Doc :: Javadoc.IO
- org.springframework.retry.annotation (Spring Retry 1.2.4.RELEASE API)
- RetryListener (Spring Retry 1.2.4.RELEASE API)
- Maven Repository: org.springframework.retry » spring-retry
- 9. Retry - Spring Batch - Reference Documentation
- spring-retry/Retryable.java at master · spring-projects/spring-retry · GitHub
- Spring-Retry - シンプルで本質的なコードを汚さないリトライ処理フレームワーク
- Spring Bootでspring-retryを使って処理をリトライする方法(@EnableRetryと@Retryableと@Recover) | 株式会社CONFRAGE ITソリューション事業部
- Spring RetryのREADME読んだ - kagamihogeの日記