JavaRush /Курси /Модуль 5. Spring /Практика: створення кастомного обробника помилок

Практика: створення кастомного обробника помилок

Модуль 5. Spring
Рівень 8 , Лекція 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".

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ