Spring Retryでリトライ実装し、テストする

# 概要

Spring Retryを用いるとリトライ処理を容易に記述することができます。
ここではSpring Retryの機能を用いた簡単なDBアクセスリトライ処理の実装と、その機能のテストを実装します。

# 実装

`@EnableRetry`を`@Configuration`が付与されているクラスに付与します。
※`@SpringBootApplication`は`@Configuration`を内包してます。

```java
package com.example.demo;

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);
	}

}
```

O/R MapperとしてMyBatisを使います。

```java
package com.example.demo.repository;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;

@Mapper
public interface DemoRepository { 

  @Select("select title from todo order by id fetch first 1 rows only;")
  String select();

}
```

------
上記を呼び出すためのクラスを用意します。

```java
package com.example.demo.repository;

import org.springframework.dao.QueryTimeoutException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

@Component
@Retryable(value = {QueryTimeoutException.class}, maxAttempts = 3, backoff = @Backoff(delay = 500)) // ①
public class ExecQuery {

  private final DemoRepository demoRepository;

  public ExecQuery(DemoRepository demoRepository) {

    this.demoRepository = demoRepository;
  }

  public String useDemoRepo() {
    return demoRepository.select();
  }

  @Recover // ②
  String recover() {
    return "recover";
  }

}
```
①：`@Retryable`アノテーションにどのようにリトライをしたいのか設定できます。
`value`にはどのようなエラーを捕捉したときにリトライを実施するかを決めます。
ここでは`QueryTimeoutException`を捕捉したときにリトライすると設定しています。
`maxAttempts`には最大試行回数を、`backoff`にはどのくらいの間隔を空けるかを設定しています。ここでは最大試行回数3回、0.5秒間隔の設定でリトライするようになっています。
②：`@Recover`アノテーションにて、最大試行回数を行っても捕捉対象エラーが出た場合にどのような対応をするか記述します。ここでは3回リトライを実施しても`QueryTimeoutException`を返してきた場合に`recover`文字列を返却します。

※`DemoRepository`に直接`@Retryable`を付与してもうまくリトライされません。
インジェクトするBeanのサブクラス関係だと思っているのですが、よくわからず。。
https://qiita.com/kazuki43zoo/items/757b557c05f548c6c5db#spring-aop%E3%81%A8%E3%81%AF

# テスト

Mockitoを使って、`DemoRepository`からエラーを返すようにしています。

```java
package com.example.demo;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

import com.example.demo.model.Demo;
import com.example.demo.repository.DemoRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;


@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ExtendWith(MockitoExtension.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
    MockitoTestExecutionListener.class})
public class RetryTest {

  @MockBean(name = "demoRepository") // name指定しないとmockが動かない。
  private DemoRepository demoRepository;

  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  void recoveryTest() {
    given(this.demoRepository.select()).willThrow(new QueryTimeoutException("qt"));

    final ResponseEntity<Demo> responseEntity =
        testRestTemplate.getForEntity("/demo", Demo.class);

    assertThat(responseEntity.getBody().getDbData()).isEqualTo("recover");

  }

  @Test
  void retryTest() {
    given(this.demoRepository.select()).willThrow(new QueryTimeoutException("qt"))
        .willReturn("title");

    final ResponseEntity<Demo> responseEntity =
        testRestTemplate.getForEntity("/demo", Demo.class);

    assertThat(responseEntity.getBody().getDbData()).isEqualTo("title");

  }
}

```

# 参考
https://github.com/nannany/spring-retry-sample
https://github.com/spring-projects/spring-retry