JavaRush /Курси /Модуль 5. Spring /Обробка виключень у контролерах з використанням @Exceptio...

Обробка виключень у контролерах з використанням @ExceptionHandler

Модуль 5. Spring
Рівень 7 , Лекція 9
Відкрита

Давай почнемо з простого питання: що трапиться, якщо твій застосунок натрапить на щось непередбачене? Наприклад, користувач запросить ресурс, якого не існує, або замість очікуваного числа введе рядок. Без належної обробки сервер з гордістю виведе в браузер яскравий stacktrace з 500-ю помилкою, що, на жаль, зовсім не порадує користувача.

Обробка виключень допомагає:

  1. Показати користувачеві зрозумілі повідомлення замість "щось пішло не так".
  2. Централізувати керування помилками.
  3. Мінімізувати хаос у коді контролерів.
  4. Забезпечити безпеку (наприклад, уникнути витоку технічної інформації).

@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();
    }
}

Розберемо по кроках:

  1. Метод hello кидає виключення, якщо параметр name відсутній.
  2. @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 дозволяє перевірити, що виключення коректно обробляються і повертаються потрібні відповіді.


Поширені помилки при обробці виключень

  1. Дублювання обробки в @ControllerAdvice і контролерах. Якщо виключення обробляється локально, глобальний обробник не спрацює.
  2. Не вказаний HTTP-статус. Якщо повертаєш JSON, не забувай вказувати правильний HTTP-статус (наприклад, 400 для некоректного запиту).
  3. Перехоплення базового Exception. Якщо додати обробку для Exception, це перекриє обробку всіх специфічних виключень. Обережно використовуй цю можливість.

Тепер ти знаєш, як правильно обробляти виключення в Spring MVC! Наступного разу замість "червоного жаху" stacktrace користувач побачить зрозуміле повідомлення і залишиться задоволений. Удачі в кодуванні! 🚀

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