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

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

Открыта

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


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

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

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

Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
У этой страницы еще нет ни одного комментария