12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-07-03

概要

  • サンプルプログラムを書いて 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

参考資料

12
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?