1. 序章

Javaでは、エラーの原因を追跡するのに役立つネストされた例外を処理するのが非常に一般的です。

この種の例外を処理する場合、例外の原因となった元の問題を知りたい場合があります。これにより、アプリケーションがケースごとに異なる応答を行うことができます。 これは、ルート例外を独自のフレームワークにラップするフレームワークを使用する場合に特に便利です。

この短い記事では、プレーンなJavaと、 Apache CommonsLangGoogleGuavaなどの外部ライブラリを使用して根本原因の例外を取得する方法を示します。

2. 年齢計算アプリ

私たちのアプリケーションは、ISO形式でStringとして受信された特定の日付からの人の年齢を示す年齢計算機になります。 日付を解析するときに発生する可能性のある2つのエラーケースを処理します。フォーマットが不十分な日付と将来の日付です。

まず、エラーケースの例外を作成しましょう。

static class InvalidFormatException extends DateParseException {

    InvalidFormatException(String input, Throwable thr) {
        super("Invalid date format: " + input, thr);
    }
}

static class DateOutOfRangeException extends DateParseException {

    DateOutOfRangeException(String date) {
        super("Date out of range: " + date);
    }

}

どちらの例外も、コードを少し明確にする共通の親例外を継承しています。

static class DateParseException extends RuntimeException {

    DateParseException(String input) {
        super(input);
    }

    DateParseException(String input, Throwable thr) {
        super(input, thr);
    }
}

その後、日付を解析するメソッドを使用してAgeCalculatorクラスを実装できます。

static class AgeCalculator {

    private static LocalDate parseDate(String birthDateAsString) {
        LocalDate birthDate;
        try {
            birthDate = LocalDate.parse(birthDateAsString);
        } catch (DateTimeParseException ex) {
            throw new InvalidFormatException(birthDateAsString, ex);
        }

        if (birthDate.isAfter(LocalDate.now())) {
            throw new DateOutOfRangeException(birthDateAsString);
        }

        return birthDate;
    }
}

ご覧のとおり、形式が間違っている場合は、DateTimeParseExceptionをカスタムInvalidFormatException。にラップします。

最後に、日付を受け取り、それを解析してから年齢を計算するパブリックメソッドをクラスに追加しましょう。

public static int calculateAge(String birthDate) {
    if (birthDate == null || birthDate.isEmpty()) {
        throw new IllegalArgumentException();
    }

    try {
        return Period
          .between(parseDate(birthDate), LocalDate.now())
          .getYears();
    } catch (DateParseException ex) {
        throw new CalculationException(ex);
    }
}

示されているように、例外を再度ラップしています。 この場合、それらを CalculationException にラップし、作成する必要があります。

static class CalculationException extends RuntimeException {

    CalculationException(DateParseException ex) {
        super(ex);
    }
}

これで、ISO形式の任意の日付を渡すことで電卓を使用する準備が整いました。

AgeCalculator.calculateAge("2019-10-01");

そして、計算が失敗した場合、問題が何であったかを知ることは役に立ちますね? それをどのように行うことができるかを知るために読み続けてください。

3. プレーンJavaを使用して根本原因を見つける

ルート原因の例外を見つけるために使用する最初の方法は、ルートに到達するまですべての原因をループするカスタムメソッドを作成することです。

public static Throwable findCauseUsingPlainJava(Throwable throwable) {
    Objects.requireNonNull(throwable);
    Throwable rootCause = throwable;
    while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
        rootCause = rootCause.getCause();
    }
    return rootCause;
}

再帰的な原因を処理するときに無限ループを回避するために、ループに条件を追加したことに注意してください。

AgeCalculator に無効な形式を渡すと、根本的な原因としてDateTimeParseExceptionが発生します。

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof DateTimeParseException);
}

ただし、将来の日付を使用すると、DateOutOfRangeExceptionが発生します。

try {
    AgeCalculator.calculateAge("2020-04-04");
} catch (CalculationException ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof DateOutOfRangeException);
}

さらに、このメソッドは、ネストされていない例外に対しても機能します。

try {
    AgeCalculator.calculateAge(null);
} catch (Exception ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof IllegalArgumentException);
}

この場合、 null を渡したため、IllegalArgumentExceptionが発生します。

4. ApacheCommonsLangを使用して根本原因を見つける

ここでは、カスタム実装を作成する代わりに、サードパーティのライブラリを使用して根本原因を見つける方法を示します。

Apache Commons Langは、 ExceptionUtils クラスを提供します。このクラスは、例外を処理するためのいくつかのユーティリティメソッドを提供します。

前の例では、 getRootCause()メソッドを使用します。

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(ExceptionUtils.getRootCause(ex) instanceof DateTimeParseException);
}

以前と同じ根本原因が発生します。 上記にリストした他の例にも同じ動作が適用されます。

5. Guavaを使用して根本原因を見つける

最後に試す方法は、Guavaを使用することです。 Apache Commons Langと同様に、 ThrowablesクラスにgetRootCause()utilityメソッドを提供します。

同じ例で試してみましょう。

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(Throwables.getRootCause(ex) instanceof DateTimeParseException);
}

動作は他の方法とまったく同じです。

6. 結論

この記事では、アプリケーションでネストされた例外を使用する方法を示し、ユーティリティメソッドを実装して根本原因の例外を見つけました。

また、ApacheCommonsLangやGoogleGuavaなどのサードパーティライブラリを使用して同じことを行う方法も示しました。

いつものように、例の完全なソースコードは、GitHubから入手できます。