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. Найдите основную причину, используя обычную 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. Подобно 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 .