«1. Введение

В Java довольно часто приходится работать с вложенными исключениями, поскольку они могут помочь нам отследить источник ошибки.

Когда мы имеем дело с такого рода исключениями, иногда нам может понадобиться узнать исходную проблему, вызвавшую исключение, чтобы наше приложение могло реагировать по-разному в каждом случае. Это особенно полезно, когда мы работаем с фреймворками, которые оборачивают корневые исключения в свои собственные.

В этой короткой статье мы покажем, как получить исключение основной причины, используя простую Java, а также внешние библиотеки, такие как Apache Commons Lang и Google Guava.

2. Приложение-калькулятор возраста

Наше приложение будет калькулятором возраста, который сообщает нам, сколько лет человеку на определенную дату, полученную в виде строки в формате ISO. Мы обработаем 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. Найдите первопричину с помощью Plain 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);
}

В этом случае мы получаем исключение IllegalArgumentException, так как мы передали значение null.

4. Поиск первопричины с помощью Apache Commons Lang

Сейчас мы продемонстрируем поиск первопричины с помощью сторонних библиотек вместо того, чтобы писать собственную реализацию.

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().

Давайте попробуем на том же примере:

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

Поведение точно такое же, как и у других методов.

6. Заключение

В этой статье мы продемонстрировали, как использовать вложенные исключения в нашем приложении, и реализовали служебный метод для поиска основной причины исключения.

Мы также показали, как сделать то же самое с помощью сторонних библиотек, таких как Apache Commons Lang и Google Guava.

Как всегда, полный исходный код примеров доступен на GitHub.