{
"timestamp": 1510417124782,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
"message": "ApplicationRepository#save(Application) failed and no fallback available.",
"path": "/application"
}
Такой вывод может быть излишним и ненужным клиентам вашего сервиса. Если вы хотите упростить жизнь сторонним сервисам в случае ошибки, то как раз об этом и пойдет речь в данном посте.
Начнем мы с построения небольшого сервиса с одним контроллером. Наш сервис будет принимать запрос на получение пользователя и в случае успеха отдавать данные по пользователю. В случае провала к нам возвращается ошибка. Начнем с простого и далее в статье будем усовершенствовать проект.
Итак, первое, что нам понадобится, это пользователь:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String firstName;
private String lastName;
}
Здесь я использовал библиотеку lombok. Аннотация Data подставляет геттеры и сеттеры в класс. Остальные аннотации добавляют пустой конструктор и конструктор с параметрами. Если вы хотите повторить данный пример у себя в IntelliJ Idea, то вам необходимо поставить галочку в пункте enable annotation processing, либо написать все руками.
Далее нам понадобится сервис (для краткости репозиторий создавать не будем):
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private static final Map<Integer, User> userStorage
= new HashMap<>();
static {
userStorage.put(1, new User(1, "Petr", "Petrov"));
userStorage.put(2, new User(2, "Ivan", "Ivanov"));
userStorage.put(3, new User(3, "Sergei", "Sidorov"));
}
public User get(int id) {
return userStorage.get(id);
}
}
Ну и, конечно, сам контроллер:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("{id}")
public User get(@PathVariable(name = "id") int id) {
return userService.get(id);
}
}
Итак, у нас есть почти полноценный сервис с пользователями. Запускаем его и смотрим.
При запросе на URL localhost:8080/user/1 нам возвращается json в таком формате:
{
"id": 1,
"firstName": "Petr",
"lastName": "Petrov"
}
Все отлично. Но что будет, если сделать запрос на URL localhost:8080/user/4 (у нас всего 3 пользователя)? Правильный ответ: мы получим статус 200 и ничего в ответе. Ситуация не особо приятная. Ошибки нет, но и запрашиваемого объекта тоже нет.
Давайте улучшим наш сервис и добавим в него выбрасывание ошибки в случае неудачи. Для начала создадим исключение:
public class ThereIsNoSuchUserException extends RuntimeException { }
Теперь добавим пробрасывание ошибки в сервис:
public User get(int id) {
User user = userStorage.get(id);
if (user == null) {
throw new ThereIsNoSuchUserException();
}
return user;
}
Сделаем перезапуск сервиса и снова посмотрим, что будет при запросе несуществующего пользователя:
{
"timestamp": 1510479979781,
"status": 500,
"error": "Internal Server Error",
"exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
"message": "No message available",
"path": "/user/4"
}
Это уже лучше. Гораздо более информативно и код статуса не 200. Такую ситуацию клиент на своей стороне уже сможет успешно и легко обработать. Но, как говорится, есть один нюанс. Ошибки могут быть совершенно разными, и клиенту нашего сервиса придется ставить кучу условных операторов и исследовать, что у нас пошло не так и как это можно поправить. Получается немного грубо с нашей стороны.
Как раз для таких случаев и была придумана аннотация ResponseStatus. Подставим ее на место нашего исключения и на практике посмотрим, как это работает:
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "There is no such user")
public class ThereIsNoSuchUserException extends RuntimeException {
}
Повторим запрос и посмотрим результат:
{
"timestamp": 1510480307384,
"status": 404,
"error": "Not Found",
"exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
"message": "There is no such user",
"path": "/user/4"
}
Отлично! Код статуса и сообщение поменялись. Теперь клиент сможет определись по коду ответа причину ошибки и даже уточнить ее по полю message. Но все же есть проблема. Большинство полей клиенту могут быть просто не нужны. Например, код ответа как отдельное поле может быть излишним, поскольку мы его и так получаем с кодом ответа. С этим нужно что-то делать.
К счастью, со spring boot сделать последний шаг к нашему успешному оповещению об ошибке не так сложно.
Все, что для этого требуется, разобрать пару аннотаций и один класс:
- Аннотация ExceptionHandler. Используется для обработки собственных и каких-то специфичных исключений. Далее в примере будет понятно, что это значит. На всякий случай ссылка на документацию.
- Аннотация ControllerAdvice. Данная аннотация дает «совет» группе констроллеров по определенным событиям. В нашем случае — это обработка ошибок. По умолчанию применяется ко всем контроллерам, но в параметрах можно указать отпределенную группу. Подбронее тут.
- Класс ResponseEntityExceptionHandler. Данный класс занимается обработкой ошибок. У него куча методов, название которых построенно по принципу handle + название исключения. Если мы хотим обработать какое-то базовое исключение, то наследуемся от этого класса и переопределяем нужный метод.
Давайте теперь посмотрим, как все это обЪединить и построить наше уникальное и неповторимое сообщение об ошибке:
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ThereIsNoSuchUserException.class)
protected ResponseEntity<AwesomeException> handleThereIsNoSuchUserException() {
return new ResponseEntity<>(new AwesomeException("There is no such user"), HttpStatus.NOT_FOUND);
}
@Data
@AllArgsConstructor
private static class AwesomeException {
private String message;
}
}
Сделаем все тот же запрос и увидим статус ответа 404 и наше сообщение с единственным полем:
{
"message": "There is no such user"
}
Аннотацию ResponseStatus над нашим исключением можно смело убирать.
В итоге у нас получилось приложение, в котором обработка ошибок настраивается максимально гибко и просто. Полный проект можно найти в репозитории github. Надеюсь, что все было просто и понятно. Спасибо за внимание и пишите комментарии! Буду рад вашим замечаниям и уточнениям!
Комментарии (6)
faoxy Автор
13.11.2017 11:19Обычно для этого используется СУБД. HashMap тут только для простой демонстрации. Но вообще, да. Если добавить еще метод на создание пользователей, то следует использовать ConcurrentHashMap.
elegorod
15.11.2017 19:58Exception тут, по-моему, явно лишний. С точки зрения сервера, если клиент прислал неправильный ИД, то он сам виноват. Никакой ошибки сервера здесь нет, значит, и Exception не нужен.
Я бы лучше в контроллере проверил, если user == null, то ответить кодом 404. В результате программа будет делать то же самое, только без лишних классов и быстрее (так как при создании Exception-а заполняется stack trace, а это относительно тяжёлая и долгая операция)faoxy Автор
15.11.2017 22:03Странный комментарий.
Во-первых, сервисы бывают разные и разные правила взаимодействия между ними могут быть. Сильно зависит от конечной системы.
Во-вторых, тут показывается пример с 404 для простоты. Вы легко можете заменить его, например, на 400 при валидации некоторых полей и в ответном сообщении показать какое поле с ошибкой.
Мне кажется вы сильно упрощаете. Если все было бы так, то и такого разнообразия интрументов не было бы. Люди просто отдавали статус при любой ошибке и все.
knekrasov
Спасибо, но одно замечание: метод
UserService.get()
не потокобезопасен. В данном игрушечном примере это скорее всего не приведет к проблеме (из HashMap вы только читаете), но вообще говоря так делать опасно.