Давай почнемо з простого питання: що трапиться, якщо твій застосунок натрапить на щось непередбачене? Наприклад, користувач запросить ресурс, якого не існує, або замість очікуваного числа введе рядок. Без належної обробки сервер з гордістю виведе в браузер яскравий stacktrace з 500-ю помилкою, що, на жаль, зовсім не порадує користувача.
Обробка виключень допомагає:
- Показати користувачеві зрозумілі повідомлення замість "щось пішло не так".
- Централізувати керування помилками.
- Мінімізувати хаос у коді контролерів.
- Забезпечити безпеку (наприклад, уникнути витоку технічної інформації).
@ExceptionHandler: твій супергерой для локальної обробки помилок
Spring MVC надає анотацію @ExceptionHandler, яка дозволяє обробляти виключення, що виникають у контролерах. Ось як це працює.
Проста обробка помилки
@Controller
public class DemoController {
@GetMapping("/hello")
public String hello(@RequestParam(required = false) String name) {
if (name == null) {
throw new IllegalArgumentException("Ім'я обов'язкове!");
}
return "Привіт, " + name;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Помилка: " + ex.getMessage();
}
}
Розберемо по кроках:
- Метод
helloкидає виключення, якщо параметрnameвідсутній. @ExceptionHandler(IllegalArgumentException.class)перехоплює виключенняIllegalArgumentException, і замість стандартного stacktrace користувачеві повертається зрозуміле повідомлення.
Спробуй викликати /hello без параметра name. Побачиш: Помилка: Ім'я обов'язкове!.
Обробка кількох типів виключень
Якщо твій контролер може кидати різні виключення, кожне з них можна обробляти окремо.
@Controller
public class MultiExceptionController {
@GetMapping("/data/{id}")
public String getData(@PathVariable("id") int id) {
if (id < 0) {
throw new IllegalArgumentException("ID не може бути від'ємним");
} else if (id == 0) {
throw new NullPointerException("ID не може бути нульовим");
}
return "Дані для ID " + id;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgument(IllegalArgumentException ex) {
return "Невірний аргумент: " + ex.getMessage();
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String handleNullPointer(NullPointerException ex) {
return "Помилка: " + ex.getMessage();
}
}
Тепер ми можемо налаштувати різні відповіді для різних типів помилок:
- Якщо користувач запросить
/data/-1, побачить:Невірний аргумент: ID не може бути від'ємним. - Якщо запросить
/data/0, побачить:Помилка: ID не може бути нульовим.
Централізація обробки помилок: @ControllerAdvice
Якщо у тебе більше двох контролерів, правильно винести загальну обробку виключень в окремий клас. Для цього використовується анотація @ControllerAdvice.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Глобальна помилка: " + ex.getMessage();
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleAllExceptions(Exception ex) {
return "Щось пішло не так: " + ex.getMessage();
}
}
Тепер IllegalArgumentException і будь-які інші виключення, що кидаються в твоєму застосунку, будуть оброблятися централізовано. Можеш залишити локальну обробку в контролерах для специфічних випадків або прибрати її зовсім.
Як повертати гарні відповіді: JSON замість тексту
В реальних застосунках ми часто повертаємо відповіді у форматі JSON, а не прості текстові повідомлення. Це особливо важливо для REST API.
Приклад з JSON відповідями
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", "Invalid argument");
response.put("message", ex.getMessage());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
Тепер, запитавши /hello без параметра name, ти отримаєш такий JSON-відповідь:
{
"error": "Invalid argument",
"message": "Ім'я обов'язкове!"
}
Використовуючи ResponseEntity, ми також можемо вказати HTTP-статус відповіді (наприклад, 400 - Bad Request).
Особливості роботи з ValidationException
Якщо ти використовуєш валідацію даних з анотацією @Valid, викинуті виключення також потрібно обробляти. Наприклад:
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String addUser(@Valid @RequestBody User user) {
return "User " + user.getName() + " додано!";
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
public class User {
@NotNull(message = "Ім'я не може бути порожнім")
private String name;
@Email(message = "Некоректний email")
private String email;
// геттери і сеттери
}
Якщо відправити запит без імені або з некоректним email, у відповіді повернеться JSON з описом усіх помилок.
Як правильно тестувати обробку виключень
Для тестування обробників ти можеш використовувати MockMvc. Ось як це зробити:
@WebMvcTest(MultiExceptionController.class)
public class ExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHandleIllegalArgument() throws Exception {
mockMvc.perform(get("/data/-1"))
.andExpect(status().isBadRequest())
.andExpect(content().string("Невірний аргумент: ID не може бути від'ємним"));
}
@Test
public void testHandleNullPointer() throws Exception {
mockMvc.perform(get("/data/0"))
.andExpect(status().isInternalServerError())
.andExpect(content().string("Помилка: ID не може бути нульовим"));
}
}
MockMvc дозволяє перевірити, що виключення коректно обробляються і повертаються потрібні відповіді.
Поширені помилки при обробці виключень
- Дублювання обробки в
@ControllerAdviceі контролерах. Якщо виключення обробляється локально, глобальний обробник не спрацює. - Не вказаний HTTP-статус. Якщо повертаєш JSON, не забувай вказувати правильний HTTP-статус (наприклад, 400 для некоректного запиту).
- Перехоплення базового
Exception. Якщо додати обробку дляException, це перекриє обробку всіх специфічних виключень. Обережно використовуй цю можливість.
Тепер ти знаєш, як правильно обробляти виключення в Spring MVC! Наступного разу замість "червоного жаху" stacktrace користувач побачить зрозуміле повідомлення і залишиться задоволений. Удачі в кодуванні! 🚀
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ