Вступление
В этой статье мы рассмотрим несколько подходов к обработке исключений в приложениях Spring REST.
В этом руководстве предполагается, что у вас есть базовые знания о Spring и вы можете создавать простые REST API, используя его.
Если вы хотите узнать больше об исключениях и настраиваемых исключениях в Java, мы подробно рассмотрели это в статьях «Обработка исключений в Java: полное руководство с лучшими и наихудшими практиками и как создавать настраиваемые исключения в Java» .
Зачем это делать?
Предположим, у нас есть простая пользовательская служба, в которой мы можем получать и обновлять зарегистрированных пользователей. У нас есть простая модель, определенная для пользователей:
public class User {
private int id;
private String name;
private int age;
// Constructors, getters, and setters
Давайте создадим контроллер REST с отображением, которое ожидает id
и
возвращает User
с данным id
если он присутствует:
@RestController
public class UserController {
private static List<User> userList = new ArrayList<>();
static {
userList.add(new User(1, "John", 24));
userList.add(new User(2, "Jane", 22));
userList.add(new User(3, "Max", 27));
}
@GetMapping(value = "/user/{id}")
public ResponseEntity<?> getUser(@PathVariable int id) {
if (id < 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
User user = findUser(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(user);
}
private User findUser(int id) {
return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(null);
}
}
Помимо простого поиска пользователя, мы также должны выполнить
дополнительные проверки, например id
всегда должен быть больше 0,
иначе мы должны вернуть BAD_REQUEST
состояния BAD_REQUEST.
Точно так же, если пользователь не найден, мы должны вернуть NOT_FOUND
состояния NOT_FOUND. Кроме того, нам может потребоваться добавить текст
для некоторых деталей об ошибке клиенту.
Для каждой проверки мы должны создать объект ResponseEntity с кодами ответа и текстом в соответствии с нашими требованиями.
Мы легко видим, что эти проверки придется выполнять несколько раз по
мере роста наших API. Например, предположим, что мы добавляем новое
сопоставление запроса PATCH для обновления наших пользователей, нам
нужно снова создать эти объекты ResponseEntity
Это создает проблему
сохранения согласованности в приложении.
Итак, проблема, которую мы пытаемся решить, - это разделение проблем.
Конечно, мы должны выполнять эти проверки в каждом RequestMapping
но
вместо обработки сценариев проверки / ошибок и того, какие ответы должны
быть возвращены в каждом из них, мы можем просто выбросить исключение
после нарушения, и эти исключения затем будут обрабатываться отдельно.
Теперь вы можете использовать встроенные исключения, уже предоставленные Java и Spring , или, если необходимо, вы можете создать свои собственные исключения и выбросить их. Это также централизует нашу логику проверки / обработки ошибок.
Кроме того, мы не можем возвращать клиенту сообщения об ошибках сервера по умолчанию при обслуживании API. Мы также не можем вернуть запутанные и трудные для понимания нашими клиентами трассировки стека. Правильная обработка исключений с помощью Spring - очень важный аспект создания хорошего REST API.
Наряду с обработкой исключений необходима документация по REST API .
Обработка исключений через @ResponseStatus
Аннотацию @ResponseStatus можно использовать в методах и классах исключений. Его можно настроить с помощью кода состояния, который будет применяться к ответу HTTP.
Давайте создадим собственное исключение для обработки ситуации, когда
пользователь не найден. Это будет исключение времени выполнения, поэтому
мы должны расширить класс java.lang.RuntimeException
Мы также отметим этот класс @ResponseStatus
:
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User Not found")
public class UserNotFoundException extends RuntimeException {
}
Когда Spring перехватывает это исключение, он использует конфигурацию,
предоставленную в @ResponseStatus
.
Изменение нашего контроллера на то же самое:
@GetMapping(value = "/user/{id}")
public ResponseEntity<?> getUser(@PathVariable int id) {
if (id < 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
User user = findUser(id);
return ResponseEntity.ok(user);
}
private User findUser(int id) {
return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElseThrow(() -> new UserNotFoundException());
}
Как мы видим, код теперь чище, с разделением задач.
@RestControllerAdvice и @ExceptionHandler
Давайте создадим настраиваемое исключение для обработки проверок
достоверности. Это снова будет RuntimeException
:
public class ValidationException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
public ValidationException(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
}
- это новая функция Spring, которую можно использовать для написания общего кода для обработки исключений.
Обычно это используется вместе с @ExceptionHandler, который фактически обрабатывает разные исключения:
@RestControllerAdvice
public class AppExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ValidationException.class)
public ResponseEntity<?> handleException(ValidationException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMsg());
}
}
Вы можете рассматривать RestControllerAdvice
как своего рода
Аспект
в вашем коде Spring. Всякий раз, когда ваш код Spring генерирует
исключение, обработчик которого определен в этом классе, соответствующая
логика может быть написана в соответствии с потребностями бизнеса.
Обратите внимание, что, в отличие от @ResponseStatus
, с помощью этого
подхода мы могли бы делать много вещей, например регистрировать наши
исключения, уведомлять и т. Д.
Что, если бы мы хотели обновить возраст существующего пользователя? У нас есть 2 проверки, которые необходимо выполнить:
id
должен быть больше 0age
должен быть от 20 до 60 лет.
Имея это в виду, давайте сделаем для этого конечную точку:
@PatchMapping(value = "/user/{id}")
public ResponseEntity<?> updateAge(@PathVariable int id, @RequestParam int age) {
if (id < 0) {
throw new ValidationException("Id cannot be less than 0");
}
if (age < 20 || age > 60) {
throw new ValidationException("Age must be between 20 to 60");
}
User user = findUser(id);
user.setAge(age);
return ResponseEntity.accepted().body(user);
}
По умолчанию @RestControllerAdvice
применим ко всему приложению, но вы
можете ограничить его определенным пакетом, классом или аннотацией.
Для ограничения уровня пакета вы можете сделать что-то вроде:
@RestControllerAdvice(basePackages = "my.package")
или же
@RestControllerAdvice(basePackageClasses = MyController.class)
Чтобы применить к определенному классу:
@RestControllerAdvice(assignableTypes = MyController.class)
Чтобы применить его к контроллерам с определенными аннотациями:
@RestControllerAdvice(annotations = RestController.class)
ResponseEntityExceptionHandler
ResponseEntityExceptionHandler обеспечивает некоторую базовую обработку исключений Spring.
Мы можем расширить этот класс и переопределить методы, чтобы настроить их:
@RestControllerAdvice
public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return errorResponse(HttpStatus.BAD_REQUEST, "Required request params missing");
}
private ResponseEntity<Object> errorResponse(HttpStatus status, String message) {
return ResponseEntity.status(status).body(message);
}
}
Чтобы зарегистрировать этот класс для обработки исключений, мы должны
аннотировать его с помощью @ResponseControllerAdvice
.
Опять же, здесь можно сделать много вещей, и это зависит от ваших требований.
Что использовать, когда?
Как видите, Spring предоставляет нам различные варианты обработки исключений в наших приложениях. Вы можете использовать один из них или их комбинацию в зависимости от ваших потребностей. Вот практическое правило:
- Для настраиваемых исключений, где ваш код состояния и сообщение
являются фиксированными, рассмотрите возможность добавления к ним
@ResponseStatus
- Для исключений, когда вам нужно вести журнал, используйте
@RestControllerAdvice
с@ExceptionHandler
. Здесь у вас также есть больше возможностей для контроля над текстом вашего ответа. - Чтобы изменить поведение ответов на исключения Spring по умолчанию,
вы можете расширить класс
ResponseEntityExceptionHandler
Примечание . Будьте осторожны при смешивании этих параметров в одном приложении. Если одно и то же обрабатывается более чем в одном месте, поведение может отличаться от ожидаемого.
Заключение
В этом руководстве мы обсудили несколько способов реализации механизма обработки исключений для REST API в Spring.
Как всегда, код примеров, использованных в этой статье, можно найти на Github .