Сегодня мы займёмся созданием кастомного обработчика ошибок, который позволит вам гибко адаптировать логику обработки исключений под нужды вашего приложения. Кроме того, вы научитесь разрабатывать собственные исключения и удостоверитесь, что пользователь видит только понятные и полезные сообщения вместо странных стектрейсов.
Почему это важно?
Ошибки — неотъемлемая часть программирования. Но то, как вы обрабатываете ошибки, может сильно повлиять на пользовательский опыт и стабильность приложения. Представьте, например, что пользователь видит что-то вроде:
org.springframework.dao.DataIntegrityViolationException: could not execute statement
Скорее всего, он подумает: «Что за чёрт? У меня компьютер сломался?!». Кастомные обработчики исключений позволяют превратить такие ошибки в ясные сообщения, например:
{
"error": "Invalid Request",
"message": "User with the same email already exists"
}
Гораздо лучше, правда?
Шаг 1: Создание пользовательского исключения
Прежде, чем обрабатывать ошибки, давайте создадим собственное исключение. Оно должно быть достаточно специфичным, чтобы мы могли легко его различать среди других типов ошибок.
Пример: UserNotFoundException
// Это наше кастомное исключение
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Обратите внимание, что это исключение расширяет RuntimeException. Это значит, что оно unchecked (непроверяемое) и не требует обязательной обработки в коде, где оно может возникнуть, что делает его более подходящим для большинства случаев в веб-приложениях.
Где выбрасывать исключение?
Предположим, у нас есть сервис для работы с пользователями:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
}
}
Если пользователь с указанным идентификатором не найден, наш сервис выбросит UserNotFoundException.
Шаг 2: Создание кастомного обработчика ошибок
Теперь мы хотим перехватить UserNotFoundException и преобразовать его в более удобный ответ для клиента (например, JSON с сообщением об ошибке).
Для этого мы используем глобальный обработчик ошибок с помощью @ControllerAdvice.
Реализация кастомного обработчика
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTP 404
public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
return new ErrorResponse("Not Found", ex.getMessage());
}
}
Вспомогательный класс ErrorResponse
Для возврата понятного ответа клиенту создадим DTO:
public class ErrorResponse {
private String error;
private String message;
public ErrorResponse(String error, String message) {
this.error = error;
this.message = message;
}
// Геттеры и сеттеры для сериализации
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Шаг 3: Тестирование кастомного обработчика ошибок
Теперь, чтобы проверить, как всё это работает, добавим контроллер:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findUserById(id);
}
}
Пример запроса:
GET /users/42
Если пользователь с ID 42 отсутствует, сервер вернёт:
{
"error": "Not Found",
"message": "User with id 42 not found"
}
Шаг 4: Логирование ошибок
Важно не только возвращать корректные ответы клиенту, но и логировать подробную информацию для разработчиков. Добавим логирование в наш обработчик:
@RestControllerAdvice
public class CustomExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
logger.error("User not found exception: {}", ex.getMessage());
return new ErrorResponse("Not Found", ex.getMessage());
}
}
Теперь каждый раз, когда возникает UserNotFoundException, ошибка будет записываться в логи.
Шаг 5: Обработка нескольких исключений
Иногда требуется обработать сразу несколько типов исключений. Просто добавьте больше методов в CustomExceptionHandler с разными @ExceptionHandler.
Пример обработки IllegalArgumentException
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // HTTP 400
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
return new ErrorResponse("Bad Request", ex.getMessage());
}
Теперь, если где-то в приложении выбрасывается IllegalArgumentException, клиент получит:
{
"error": "Bad Request",
"message": "Invalid input data"
}
Шаг 6: Тестирование обработчиков ошибок
Для проверки работы обработчиков используйте инструменты, такие как Postman или cURL, либо напишите интеграционные тесты.
Пример теста с MockMvc
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
Mockito.when(userService.findUserById(42L))
.thenThrow(new UserNotFoundException("User with id 42 not found"));
mockMvc.perform(get("/users/42"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Not Found"))
.andExpect(jsonPath("$.message").value("User with id 42 not found"));
}
}
Практическое применение
В реальных проектах кастомные обработчики ошибок применяются, чтобы:
- Избежать утечек технических деталей (например, стектрейсов) клиенту.
- Обеспечить единообразие в обработке ошибок и форматировании ответов.
- Чётко разделить обработку ошибок между разработчиками (разные контроллеры могут иметь свои кастомные обработчики).
- Легко логировать и отслеживать причины возникновения ошибок.
Теперь ваше приложение готово к работе с самыми разными исключениями, а пользователи будут благодарны за дружелюбные сообщения вместо загадочных "500 Internal Server Error".
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ