Сьогодні займемося створенням кастомного обробника помилок, який дозволить тобі гнучко адаптувати логіку обробки винятків під потреби твого додатку. Крім того, ти навчишся розробляти власні винятки і переконаєшся, що користувач бачить тільки зрозумілі й корисні повідомлення замість дивних стектрейсів.
Чому це важливо?
Помилки — невід'ємна частина програмування. Але те, як ти обробляєш помилки, може сильно вплинути на користувацький досвід і стабільність додатку. Уяви, наприклад, що користувач бачить щось на кшталт:
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".
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ