JavaのSupplierを使って汎用的なリトライ処理を実装する

Java

アプリケーションの機能を実装していく中で、様々なリトライ処理が必要になってきます。
DBアクセスに失敗した場合のリトライ、HTTP通信(APIなど)に失敗した場合のリトライ、など様々かと思います。


Javaでこういったリトライ処理を実装しようとすると、普通にやるとfor文やwhile文を使ってしまいがちです。
それでもいいですが、Javaのユーティリティクラス群であるSupplierを使えば、もうちょっと汎用的に実装することができます。


今回は、Javaのユーティリティクラスである「java.util.function.Supplier」を使って、汎用的なリトライ処理を実装する方法を紹介していきます。


環境情報


  • OS:Windows10
  • Java:Java1.8

ユーティリティクラス群で使用されている関数型インターフェースは、Java8から導入された機能となります。
Java1.7以前は使用できないので注意してください。


java.util.functionは関数型インターフェース


java.util.function配下の機能は全て、関数型インターフェースで実装されています。
イメージとしては、実行するメソッドをパラメータとして渡す形です。


java.util.functionのAPI仕様をみると複雑なインターフェースが沢山あるようにみえるのですが、大きくわけて4つに分類されます。
今回のリトライ処理では「Supplier」を使うのですが、他3つのインターフェースは以下になります。


種類

実装するメソッド

概要

Function<T,R>

R apply(T t)

実装するメソッドは、引数としてTを受け取り、結果としてRを返すものになる

Consumer<T>

void accept(T t)

実装するメソッドは、引数としてTを受け取り、結果を返さず終了するものになる

Predicate<T>

boolean test(T t)

実装するメソッドは、引数としてTを受け取り、boolean値を結果として返すものになる

Supplier<T>

T get()

実装するメソッドは、何も引数として受け取らず、結果としてTを返すものになる


関数型インターフェースの簡単なサンプルを、以下に紹介します。
先ほど説明した通り、関数をパラメータとして渡して渡された側で関数を実行しています。


return new RetryUtil()
    .retryFunc(() -> {
        ※実行する機能
    );
public class RetryUtil {
    private <T> T retryFunc(Supplier<T> func) {
        return func.get();
    }
}

リトライ処理


リトライ処理は、java.util.function配下の「Supplier」を使用します。
「Supplier」を使用し、実施する処理自体は呼び出し元で定義し、リトライの仕組み自体は共通クラス(ユーティリティクラス)に実装しています。


まずはリトライ処理を実施するメイン処理です。

import java.util.function.Supplier;

/**
    リトライ処理サンプル
*/
public class RetrySample {

    /**
        リトライ処理メイン
    */
    public static void main(String[] args) 
        throws InterruptedException {
        System.out.print("start: RetrySample\r\n");

        // 処理結果を失敗で初期化
        boolean isDone = false;

        // リトライ処理
        try {
            isDone = retryExec();
        } catch (Exception e) {
            System.out.print("RetrySample: main is failer\r\n");
        } finally {
            System.out.print(
                "RetrySample: result is " + isDone + "\r\n");
        }

        System.out.print("end: RetrySample\r\n");
    }

    /**
        リトライ処理の実施
    */
    private static boolean retryExec () 
        throws InterruptedException {

        // 1.リトライ回数とスリープ時間を設定
        int retryNum = 3;
        long sleepMilSec = 1000;

        // 2.ユーティリティを使って〇〇〇処理を実施する
        return new RetryUtil(retryNum, sleepMilSec)
            .retryFunc(() -> {
                try {
                    System.out.print("retryExec\r\n");
                    boolean isSucess = 〇〇〇処理を実施
                    if (isSucess) {
                        return true;
                    } else {
                        throw new RetryException();
                    }
                } catch (Exception e) {
                    throw new RetryException();
                }
            }, "RetrySample");
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライユーティリティのオブジェクトに設定する初期値を定義しています。
今回はリトライ処理ということで、ありがちなリトライ回数とスリープ時間はユーティリティクラスのコンストラクタで設定できようにしています。
その他、初期値として設定したい値があるのであれば、同様にコンストラクタで定義するのがよいかと思います。


「2.ユーティリティを使って〇〇〇処理を実施する」の機能が、実際に関数型インフェースで定義しているリトライ処理する中身です。
サンプルなので「〇〇〇処理を実施」としていますが、実際にはここにリトライ制御したい機能を実装していくのがよいでしょう。
リトライする契機としては、独自例外クラスである「RetryException」が発生した場合になります。
失敗したら(isDoneがfalseだったら)「RetryException」をスローすることで、リトライ処理を実行するようにユーティリティクラスを準備しています。


次にリトライユーティリティクラスです。

import java.text.MessageFormat;
import java.util.function.Function;
import java.util.function.Supplier;

/**
    リトライユーティリティ
*/
public class RetryUtil {

    // リトライ回数
    private final int retryNum;

    // リトライ時のスリープ時間
    private final long retryWait;

    /**
     * コンストラクタ
     */
    public RetryUtil(int retryNum, long retryWait) {

        // 1.リトライ回数とスリープ時間を設定
        this.retryNum = retryNum;
        this.retryWait = retryWait;
    }

    /**
     * リトライ処理
     */
    public <T> T retryFunc(Supplier<T> func, String funcName)
            throws RetryException, InterruptedException {
        return retryFunc(func, funcName, 1, null);
    }

    /**
     * リトライユーティリティ
     */
    private <T> T retryFunc(Supplier<T> func, 
        String funcName, int count, Exception befEx)
        throws RetryException, InterruptedException {

        // 2.Supplierで提供されたfuncを実行
        try {
            return func.get();

        // 3.例外が発生した場合はリトライ回数に
        //     達するまで自身を再度呼び出す
        } catch (RetryException e) {
            System.out.print("RetryException\r\n");
            if (count > this.retryNum) {
                System.out.print("retry end\r\n");
                throw new RetryException(e);
            }
            count++;
            Thread.sleep(this.retryWait);
            System.out.print("next retry " + 
                Integer.toString(count) + "回目の処理開始\r\n");
            return retryFunc(func, funcName, count, null);
        }
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライ回数とスリープ時間をクラス変数として格納しています。
実際のリトライ処理時に、この値を参照しています。


「2.Supplierで提供されたfuncを実行」で、実際に関数型インターフェースで渡されたメソッドを実行しています。
実行方法は、パラメータ名.get() で実行されます。


「3.例外が発生した場合はリトライ回数に達するまで自身を再度呼び出す」がリトライ処理のメインとなる部分ですね。
個別例外である「RetryException」が発生するとリトライ処理を実施します。
リトライ回数が上限に達すると上位に例外をスローしてリトライ処理を中断します。
上限に達していない場合は再度自身を呼び出して同じ処理を実行しています。


最後に個別の例外クラスです。
リトライ処理を制御しやすくするために、個別例外を準備しています。


/**
    リトライユーティリティ固有例外
*/
public class RetryException extends RuntimeException {

    /**
     * コンストラクタ
     */
    public RetryException() {
        super();
    }

    /**
     * コンストラクタ(例外指定)
     * @param cause 例外
     */
    public RetryException(Throwable cause) {
        super(cause);
    }
}