Обработка исключений в контроллерах Spring

image

Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые предоставляет Spring Framework для решения этой задачи и на простых примерах посмотрим как всё работает. Кому интересна данная тема — добро пожаловать под кат!

Изначально до Spring 3.2 основными способами обработки исключений в приложении были HandlerExceptionResolver и аннотация @ExceptionHandler. Их мы ещё подробно разберём ниже, но они имеют определённые недостатки. Начиная с версии 3.2 появилась аннотация @ControllerAdvice, в которой устранены ограничения из предыдущих решений. А в Spring 5 добавился новый класс ResponseStatusException, который очень удобен для обработки базовых ошибок для REST API.

А теперь обо всём по порядку, поехали!

Обработка исключений на уровне контроллера — @ExceptionHandler

С помощью аннотации @ExceptionHandler можно обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод, в котором будет содержаться вся логика обработки нужного исключения, и проаннотировать его.

В качестве примера разберём простой контроллер:

Тут я сделал метод testExceptionHandler, который вернёт либо исключение BusinessException, либо успешный ответ — всё зависит от того что было передано в параметре запроса. Это нужно для того, чтобы можно было имитировать как штатную работу приложения, так и работу с ошибкой.

А вот следующий метод handleException предназначен уже для обработки ошибок. У него есть аннотация @ExceptionHandler(BusinessException. class), которая говорит нам о том что для последующей обработки будут перехвачены все исключения типа BusinessException. В аннотации @ExceptionHandler можно прописать сразу несколько типов исключений, например так: @ExceptionHandler().

Сама обработка исключения в данном случае примитивная и сделана просто для демонстрации работы метода — по сути вернётся код 200 и JSON с описанием ошибки. На практике часто требуется более сложная логика обработки и если нужно вернуть другой код статуса, то можно воспользоваться дополнительно аннотацией @ResponseStatus, например @ResponseStatus(HttpStatus. INTERNAL_SERVER_ERROR).

Пример работы с ошибкой:

Пример штатной работы:

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

Обработка исключений с помощью HandlerExceptionResolver

HandlerExceptionResolver является общим интерфейсом для обработчиков исключений в Spring. Все исключений выброшенные в приложении будут обработаны одним из подклассов HandlerExceptionResolver. Можно сделать как свою собственную реализацию данного интерфейса, так и использовать существующие реализации, которые предоставляет нам Spring из коробки. Давайте разберем их для начала:

ExceptionHandlerExceptionResolver — этот резолвер является частью механизма обработки исключений с помощью аннотации @ExceptionHandler, о которой я уже упоминал ранее.

DefaultHandlerExceptionResolver — используется для обработки стандартных исключений Spring и устанавливает соответствующий код ответа, в зависимости от типа исключения:

Exception HTTP Status Code
BindException 400 (Bad Request)
ConversionNotSupportedException 500 (Internal Server Error)
HttpMediaTypeNotAcceptableException 406 (Not Acceptable)
HttpMediaTypeNotSupportedException 415 (Unsupported Media Type)
HttpMessageNotReadableException 400 (Bad Request)
HttpMessageNotWritableException 500 (Internal Server Error)
HttpRequestMethodNotSupportedException 405 (Method Not Allowed)
MethodArgumentNotValidException 400 (Bad Request)
MissingServletRequestParameterException 400 (Bad Request)
MissingServletRequestPartException 400 (Bad Request)
NoSuchRequestHandlingMethodException 404 (Not Found)
TypeMismatchException 400 (Bad Request)

Основной недостаток заключается в том что возвращается только код статуса, а на практике для REST API одного кода часто не достаточно. Желательно вернуть клиенту еще и тело ответа с описанием того что произошло. Эту проблему можно решить с помощью ModelAndView, но не нужно, так как есть способ лучше.

ResponseStatusExceptionResolver — позволяет настроить код ответа для любого исключения с помощью аннотации @ResponseStatus.

В качестве примера я создал новый класс исключения ServiceException:

В ServiceException я добавил аннотацию @ResponseStatus и в value указал что данное исключение будет соответствовать статусу INTERNAL_SERVER_ERROR, то есть будет возвращаться статус-код 500.

Для тестирования данного нового исключения я создал простой контроллер:

Если отправить GET-запрос и передать параметр exception=true, то приложение в ответ вернёт 500-ю ошибку:

Из недостатков такого подхода — как и в предыдущем случае отсутствует тело ответа. Но если нужно вернуть только код статуса, то @ResponseStatus довольно удобная штука.

Кастомный HandlerExceptionResolver позволит решить проблему из предыдущих примеров, наконец-то можно вернуть клиенту красивый JSON или XML с необходимой информацией. Но не спешите радоваться, давайте для начала посмотрим на реализацию.

В качестве примера я сделал кастомный резолвер:

Ну так себе, прямо скажем. Код конечно работает, но приходится выполнять всю работу руками: сами проверяем тип исключения, и сами формируем объект древнего класса ModelAndView. На выходе конечно получим красивый JSON, но в коде красоты явно не хватает.

Такой резолвер может глобально перехватывать и обрабатывать любые типы исключений и возвращать как статус-код, так и тело ответа. Формально он даёт нам много возможностей и не имеет недостатков из предыдущих примеров. Но есть способ сделать ещё лучше, к которому мы перейдем чуть позже. А сейчас, чтобы убедиться что всё работает — напишем простой контроллер:

А вот и пример вызова:

Видим что исключение прекрасно обработалось и в ответ получили код 400 и JSON с сообщением об ошибке.

Обработка исключений с помощью @ControllerAdvice

Наконец переходим к самому интересному варианту обработки исключений — эдвайсы. Начиная со Spring 3.2 можно глобально и централизованно обрабатывать исключения с помощью классов с аннотацией @ControllerAdvice.

Разберём простой пример эдвайса для нашего приложения:

Как вы уже догадались, любой класс с аннотацией @ControllerAdvice является глобальным обработчиком исключений, который очень гибко настраивается.
В нашем случае мы создали класс DefaultAdvice с одним единственным методом handleException. Метод handleException имеет аннотацию @ExceptionHandler, в которой, как вы уже знаете, можно определить список обрабатываемых исключений. В нашем случае будем перехватывать все исключения BusinessException.

Можно одним методом обрабатывать и несколько исключений сразу: @ExceptionHandler(). Так же можно в рамках эдвайса сделать сразу несколько методов с аннотациями @ExceptionHandler для обработки разных исключений.
Обратите внимание, что метод handleException возвращает ResponseEntity с нашим собственным типом Response:

Таким образом у нас есть возможность вернуть клиенту как код статуса, так и JSON заданной структуры. В нашем простом примере я записываю в поле message описание ошибки и возвращаю HttpStatus. OK, что соответствует коду 200.

Для проверки работы эдвайса я сделал простой контроллер:

В результате, как и ожидалось, получаем красивый JSON и код 200:

А что если мы хотим обрабатывать исключения только от определенных контроллеров?
Такая возможность тоже есть! Смотрим следующий пример:

Обратите внимание на аннотацию @ControllerAdvice(annotations = CustomExceptionHandler. class). Такая запись означает что CustomAdvice будет обрабатывать исключения только от тех контроллеров, которые дополнительно имеют аннотацию @CustomExceptionHandler.

Аннотацию @CustomExceptionHandler я специально сделал для данного примера:

А вот и исходный код контроллера:

В контроллере Example5Controller присутствует аннотация @CustomExceptionHandler, а так же на то что выбрасывается то же исключение что и в Example4Controller из предыдущего примера. Однако в данном случае исключение BusinessException обработает именно CustomAdvice, а не DefaultAdvice, в чём мы легко можем убедиться.

Для наглядности я немного изменил сообщение об ошибке в CustomAdvice — начал добавлять к нему дату:

Исключение ResponseStatusException.

Сейчас речь пойдёт о формировании ответа путём выброса исключения ResponseStatusException:

Выбрасывая ResponseStatusException можно также возвращать пользователю определённый код статуса, в зависимости от того, что произошло в логике приложения. При этом не нужно создавать кастомное исключение и прописывать аннотацию @ResponseStatus — просто выбрасываем исключение и передаём нужный статус-код. Конечно тут возвращаемся к проблеме отсутствия тела сообщения, но в простых случаях такой подход может быть удобен.

Резюме: мы познакомились с разными способами обработки исключений, каждый из которых имеет свои особенности. В рамках большого приложения можно встретить сразу несколько подходов, но при этом нужно быть очень осторожным и стараться не переусложнять логику обработки ошибок. Иначе получится что какое-нибудь исключение обработается не в том обработчике и на выходе ответ будет отличаться от ожидаемого. Например если в приложении есть несколько эдвайсов, то при создании нового нужно убедиться, что он не сломает существующий порядок обработки исключений из старых контроллеров.
Так что будьте внимательны и всё будет работать замечательно!

Обработка ошибок API в веб-приложении, используя Axios

Но есть еще так много вещей, которые находятся вне вашего контроля, которые могут вызвать ошибки при выполнении запросов API — и вы, вероятно, даже не знаете, что они происходят!

Эта статья посвящена в основном ошибкам, которые вы видите в браузере. На бэкенде тоже все может выглядеть довольно забавно. Просто взгляните на три вещи, которые вы можете увидеть в своих бэкенд журналах.

Ниже приведены три типа ошибок, которые могут появиться, и как их обрабатывать при использовании axios.

Отлов ошибок Axios

Ниже приведен фрагмент кода, который я начал включать в несколько проектов JS:

Каждое условие предназначено для фиксации различного типа ошибки.

Проверка error. response

Применяйте следующее: “Показать страницу 404 Not Found / сообщение об ошибке, если ваш API возвращает 404.” Покажите другое сообщение об ошибке, если ваш бэкенд возвращает 5xx или вообще ничего не возвращает. Вы может показаться, что ваш хорошо сконструированный бэкенд не будет генерировать ошибки, но это всего лишь вопрос времени, а не “если”.

Проверка error. request

• Вы находитесь в обрывочной сети (например, в метро или используете беспроводную сеть здания).

• Ваш бэкенд зависает на каждом запросе и не возвращает ответ вовремя.

• Вы делаете междоменные запросы, но вы не авторизованы, чтобы их делать.

• Вы делаете междоменные запросы, и вы авторизованы, но бэкенд API возвращает ошибку.

Одна из наиболее распространенных версий этой ошибки имела бесполезное сообщение “Ошибка сети”. У нас есть API для фронтенда и бэкенда, размещенные в разных доменах, поэтому каждый вызов к бэкенд API — это междоменный запрос.

Из-за ограничений безопасности на JS в браузере, если вы делаете запрос API, и он не работает из-за плохих сетей, единственная ошибка, которую вы увидите — это “Ошибка сети”, которая невероятно бесполезна. Она может означать что угодно: от “Ваше устройство не имеет подключения к Интернету” до “Ваши OPTIONS вернули 5xx” (если вы делаете запросы CORS). Причина ошибки сети хорошо описана в этом ответе на StackOverflow.

Все остальные типы ошибок

Как вам их исправить?

Ухудшение пользовательского опыта

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

Например, если запрос не выполняется и страница бесполезна без этих данных, то у нас будет большая страница ошибок, которая появится и предложит пользователям выход — иногда это всего лишь кнопка “Обновить страницу”.

Другой пример: если запрос на изображение профиля в потоке социальных сетей не выполняется, мы можем показать изображение-плейсхолдер и отключить изменения изображения профиля вместе с всплывающим уведомлением, объясняющим, почему кнопка “Обновить изображение профиля” отключена. Однако показывать предупреждение с надписью “422 необработанных объекта” бесполезно для пользователя.

Обрывистые сети

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

Для решения такого рода периодических проблем с сетью, мы добавили axios-retry, что решило большое количество ошибок, которые мы наблюдали в продакшне. Это было добавлено в нашу настройку axios:

Мы увидели, что 10% наших пользователей (которые находятся в плохих школьных сетях) периодически наблюдали ошибки сети, но число снизилось до <2% после добавления автоматических повторных попыток при сбое.

Скриншот количества ошибок сети, как они появляются в браузере New Relic. <1% запросов неверны. Это подводит меня к последнему пункту.

Добавляйте отчеты об ошибках в свой интерфейс

Полезно иметь отчеты об ошибках и событиях фронтенда, чтобы вы знали, что происходит в разработке, прежде чем ваши пользователи сообщат вам о них. На моей основной работе мы используем браузер New Relic для отправки событий ошибок с фронтенда. Поэтому всякий раз, когда мы ловим исключение, мы регистрируем сообщение об ошибке вместе с трассировкой стека (хотя это иногда бесполезно с минимизированными пакетами) и некоторыми метаданными о текущем сеансе, чтобы попытаться воссоздать его.

Другие инструменты, используемые нами — Sentry + SDK браузер, Rollbar и целая куча других полезных инструментов, перечисленных на GitHub.

Заключение

Если вы больше ничего не можете выжать из этого, сделайте одно: перейдите в свою кодовую базу и просмотрите, как вы обрабатываете ошибки с помощью axios.

Источники:

https://habr. com/ru/post/528116/

https://medium. com/nuances-of-programming/%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%BE%D1%88%D0%B8%D0%B1%D0%BE%D0%BA-api-%D0%B2-%D0%B2%D0%B5%D0%B1-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B8-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8F-axios-932e9d66a526

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: