«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.