Давайте начнем с простого вопроса: что произойдет, если ваше приложение встретит нечто непредвиденное? Например, пользователь запросит ресурс, которого не существует, или вместо ожидаемого числа введет строку. Без надлежащей обработки, ваш сервер с гордостью выплюнет в браузер красочный стектрейс с 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, и вместо стандартного стектрейса пользователю возвращается понятное сообщение.
Попробуйте вызвать /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! В следующий раз вместо "красного ужаса" стек-трейса пользователь увидит понятное сообщение и останется доволен. Удачи в кодинге! 🚀
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ