Spring Retry を利用して宣言型のリトライ処理を実装する

## 概要

- サンプルプログラムを書いて 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](https://github.com/spring-projects/spring-retry) に載っている。

```gradle

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

```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 アノテーションを指定している。

```java
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 アノテーションを指定したメソッドにて処理した結果を返す。

```java
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 を発生させる。

```java
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 \- シンプルで本質的なコードを汚さないリトライ処理フレームワーク](https://tech.atware.co.jp/spring-retry/) や [org\.springframework\.retry\.annotation \(Spring Retry 1\.2\.4\.RELEASE API\)](https://static.javadoc.io/org.springframework.retry/spring-retry/1.2.4.RELEASE/index.html?org/springframework/retry/annotation/package-summary.html)　が参考になる。

```java
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 を継承しているだけ。

```java
package info.maigo.lab.sample.springretry;

public class PonException extends RuntimeException {
}
```

### KotsuException.java

コツ例外。RuntimeException を継承しているだけ。

```java
package info.maigo.lab.sample.springretry;

public class KotsuException extends RuntimeException {
}
```

### PonKotsuRetryListener.java

RetryListener インターフェースを実装したクラス。
リトライ処理中に呼び出されるコールバックで構成されている。
今回の実装ではリトライ処理の状況を標準出力に出力している。

```java
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](https://github.com/spring-projects/spring-retry)
- [org\.springframework\.retry:spring\-retry:1\.2\.4\.RELEASE API Doc :: Javadoc\.IO](https://www.javadoc.io/doc/org.springframework.retry/spring-retry/1.2.4.RELEASE)
- [org\.springframework\.retry\.annotation \(Spring Retry 1\.2\.4\.RELEASE API\)](https://static.javadoc.io/org.springframework.retry/spring-retry/1.2.4.RELEASE/index.html?org/springframework/retry/annotation/package-summary.html)
- [RetryListener \(Spring Retry 1\.2\.4\.RELEASE API\)](https://static.javadoc.io/org.springframework.retry/spring-retry/1.2.4.RELEASE/index.html?org/springframework/retry/RetryListener.html)
- [Maven Repository: org\.springframework\.retry » spring\-retry](https://mvnrepository.com/artifact/org.springframework.retry/spring-retry)
- [9. Retry  \- Spring Batch \- Reference Documentation](https://docs.spring.io/spring-batch/trunk/reference/htmlsingle/#retry)
- [spring\-retry/Retryable\.java at master · spring\-projects/spring\-retry · GitHub](https://github.com/spring-projects/spring-retry/blob/master/src/main/java/org/springframework/retry/annotation/Retryable.java)
- [Spring\-Retry \- シンプルで本質的なコードを汚さないリトライ処理フレームワーク](https://tech.atware.co.jp/spring-retry/)
- [Spring Bootでspring\-retryを使って処理をリトライする方法\(@EnableRetryと@Retryableと@Recover\) \| 株式会社CONFRAGE ITソリューション事業部](https://confrage.jp/spring-boot%E3%81%A7spring-retry%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%87%A6%E7%90%86%E3%82%92%E3%83%AA%E3%83%88%E3%83%A9%E3%82%A4%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95enableretry%E3%81%A8retryable/)
- [Spring RetryのREADME読んだ \- kagamihogeの日記](http://kagamihoge.hatenablog.com/entry/2018/05/30/222831)