Spring Retry トラブルシューティング

## 概要
- Spring Retry で発生する様々なトラブルについての対応策を列挙する
- 今回の環境: Java 8 + Spring Boot 2.2.0 + Spring Retry 1.2.4

## コンパイルできない

以下のようなエラーメッセージが出力される。

```
Error:(3, 44) java: パッケージorg.springframework.retry.annotationは存在しません
Error:(11, 4) java: シンボルを見つけられません
  シンボル:   クラス Retryable
```

Spring Retry が導入されていないと思われるため、Spring Retry を導入すれば良い。

Apache Maven の場合は pom.xml の dependencies 要素に以下を追加する。

```xml
<!-- https://mvnrepository.com/artifact/org.springframework.retry/spring-retry -->
<dependency>
  <groupId>org.springframework.retry</groupId>
  <artifactId>spring-retry</artifactId>
  <version>1.2.4.RELEASE</version>
</dependency>
```

Gradle の場合は build.gradle の dependencies に以下を追加する。

```build.gradle
implementation 'org.springframework.retry:spring-retry:1.2.4.RELEASE'
```

## 実行できない (BeanCreationException が発生する)

以下のようなエラーが発生する。

```
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.aop.config.internalAutoProxyCreator': Initialization of bean failed; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/annotation/Pointcut
```

```
Caused by: java.lang.NoClassDefFoundError: org/aspectj/lang/annotation/Pointcut
```

```
Caused by: java.lang.ClassNotFoundException: org.aspectj.lang.annotation.Pointcut
```

Spring Retry の実行時には AspectJ の aspectjweaver が必要。
Spring Boot を使っているなら Spring Boot AOP Starter を導入するのが良い。

Apache Maven の場合は pom.xml の dependencies 要素に以下を追加する。

```xml
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
  <scope>runtime</scope>
</dependency>
```

Gradle の場合は build.gradle の dependencies に以下を追加する。

```build.gradle
runtime('org.springframework.boot:spring-boot-starter-aop')
```

## リトライされない

### @EnableRetry を指定する

Spring Retry を使うためには以下のようにアプリケーションクラス等に @EnableRetry アノテーションを指定する必要がある。

```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}
```

### @Service @Repository @Component などを指定する

DI コンテナに登録していないクラスはリトライ対象にならない。
以下のようにリトライ処理するクラスに @Service @Repository @Component などのアノテーションを指定する。

```java
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

  @Retryable(
    value = {RuntimeException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000))
  public String hello(String foo, String bar) {
    throw new RuntimeException("This is a sample error.");
  }

  @Recover
  public String recover(RuntimeException e, String foo, String bar) {
    throw e;
  }
}
```

### @Retryable を指定する

以下のようにメソッドに @Retryable アノテーションでリトライする例外の型を value または include で指定する (value は include の別名として使用可能)。また、リトライ対象外の例外の型を exclude で指定することも可能。

```java
@Retryable(
  include = {Exception.class},
  exclude = {RuntimeException.class},
  maxAttempts = 3,
  backoff = @Backoff(delay = 1000))
public String hello(String foo, String bar) {
  throw new RuntimeException("This is a sample error.");
}
```

```java
@Retryable(
  value = {Exception.class},
  exclude = {RuntimeException.class},
  maxAttempts = 3,
  backoff = @Backoff(delay = 1000))
public String hello(String foo, String bar) {
  throw new RuntimeException("This is a sample error.");
}
```

### @Autowired を指定する

DI コンテナに登録していないクラスはリトライ対象にならないため、使用する箇所では以下のように @Autowired アノテーションを指定する。

```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DemoController {

  // リトライ処理が書いてあるクラスのオブジェクトに @Autowired を指定
  @Autowired
  private MyRetryableService service;

  @RequestMapping("/")
  public String index() {
    return service.hello("hello", "goodbye");
  }
}
```

## @Recover をつけたメソッドが呼ばれない (ExhaustedRetryException が発生する)

以下のようなエラーが発生する。

```
org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException: This is a sample error.
    at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:61)
    at org.springframework.retry.interceptor.RetryOperationsInterceptor$ItemRecovererCallback.recover(RetryOperationsInterceptor.java:141)
    at org.springframework.retry.support.RetryTemplate.handleRetryExhausted(RetryTemplate.java:512)
    at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:351)
    at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:180)
```

@Recover アノテーションを指定したメソッドを、リトライ対象のメソッドシグネチャに合わせる必要がある。
具体的には、以下のように「戻り値の型」と「リトライ対象の例外の型」を引数として記述する。

```java
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

  // 戻り値が String
  // 例外が RuntimeException
  @Retryable(
    value = {RuntimeException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000))
  public String hello(String foo, String bar) {
    System.out.println("DemoService#hello");
    throw new RuntimeException("This is a sample error.");
  }

  // 戻り値が String
  // 引数にリトライ対象の例外である RuntimeException を記述
  @Recover
  public String recover(RuntimeException e) {
    throw e;
  }
}
```

また、元のメソッドの引数が @Recover を指定したメソッドには存在する必要はないが、付加することで元のメソッドに渡された値が引数として渡されるので情報を取得できるようになる。

```java
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

  // 戻り値が String
  // 例外が RuntimeException
  // 引数が String foo と String bar
  @Retryable(
    value = {RuntimeException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000))
  public String hello(String foo, String bar) {
    System.out.println("DemoService#hello");
    throw new RuntimeException("This is a sample error.");
  }

  // 戻り値が String
  // 引数にリトライ対象の例外である RuntimeException を記述
  // 例外の引数のあとに、リトライ対象メソッド引数の String foo と String bar を追加
  @Recover
  public String recover(RuntimeException e, String foo, String bar) {
    System.out.println("foo=" + foo);
    System.out.println("bar=" + bar);
    throw e;
  }
}
```

## 参考資料
- [GitHub \- spring\-projects/spring\-retry](https://github.com/spring-projects/spring-retry)
- [spring\-retry 1\.2\.4\.RELEASE javadoc \(org\.springframework\.retry\)](https://www.javadoc.io/doc/org.springframework.retry/spring-retry/1.2.4.RELEASE/index.html)
- [Spring Retry の include, exclude, ExhaustedRetryException の挙動を確認する \- Qiita](https://qiita.com/niwasawa/items/cb784842c2351551923a)
- [Spring Retry を利用して宣言型のリトライ処理を実装する \- Qiita](https://qiita.com/niwasawa/items/4938f5a79fa225471dd6)