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

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

Модуль 5. Spring
7 уровень , 9 лекция
Открыта

Давайте начнем с простого вопроса: что произойдет, если ваше приложение встретит нечто непредвиденное? Например, пользователь запросит ресурс, которого не существует, или вместо ожидаемого числа введет строку. Без надлежащей обработки, ваш сервер с гордостью выплюнет в браузер красочный стектрейс с 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, и вместо стандартного стектрейса пользователю возвращается понятное сообщение.

Попробуйте вызвать /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! В следующий раз вместо "красного ужаса" стек-трейса пользователь увидит понятное сообщение и останется доволен. Удачи в кодинге! 🚀

Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
10 декабря 2025
В первых двух примерах урока в контроллерах у get методов должна быть еще аннотация @ResponseBody, иначе jvm пытается передать в ответе не строку, а html страницу, а нам все же нужна строка
Артём Уровень 112
10 сентября 2025
Пример с JSON ответами поправить надо было бы. Сигнатура метода поломалась и форматирование тоже.