JavaRush /Курсы /Модуль 5. Spring /Практика: создание кастомного обработчика ошибок

Практика: создание кастомного обработчика ошибок

Модуль 5. Spring
9 уровень , 6 лекция
Открыта

Сегодня мы займёмся созданием кастомного обработчика ошибок, который позволит вам гибко адаптировать логику обработки исключений под нужды вашего приложения. Кроме того, вы научитесь разрабатывать собственные исключения и удостоверитесь, что пользователь видит только понятные и полезные сообщения вместо странных стектрейсов.


Почему это важно?

Ошибки — неотъемлемая часть программирования. Но то, как вы обрабатываете ошибки, может сильно повлиять на пользовательский опыт и стабильность приложения. Представьте, например, что пользователь видит что-то вроде:


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".

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ