Исключения, Interceptors и Filters¶
17. Обработка исключений¶
17.1. Ошибки в Spring MVC¶
Исключения могут возникнуть: - при binding/validation, - в контроллере, - в сервисном слое, - при сериализации ответа.
DispatcherServlet перехватывает исключения и передает в HandlerExceptionResolver.
17.2. @ExceptionHandler¶
Локальный обработчик ошибок в контроллере:
@Controller
public class UserController {
@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id); // throws UserNotFoundException
}
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
}
Работает только для методов этого контроллера.
Аннотация @ExceptionHandler указывает, какие exception-типы обрабатывает метод. Метод может возвращать:
- view name / ModelAndView для HTML,
- DTO / ProblemDetail для REST,
- ResponseEntity<?>, если нужно управлять status и headers.
17.3. @ControllerAdvice¶
Глобальный обработчик для всех контроллеров:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(UserNotFoundException ex, Model model) {
model.addAttribute("error", ex.getMessage());
return "error/404";
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleGeneral(Exception ex) {
return "error/500";
}
}
Можно ограничить область: @ControllerAdvice("com.example.controller") или @ControllerAdvice(assignableTypes = UserController.class).
17.4. @RestControllerAdvice¶
@RestControllerAdvice = @ControllerAdvice + @ResponseBody.
Все методы возвращают JSON, а не view name.
@RestControllerAdvice
public class GlobalRestExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> details = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("VALIDATION_ERROR", details);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
return new ErrorResponse("INTERNAL_ERROR", "Unexpected error occurred");
}
}
17.4.1. Аннотации и классы для ошибок¶
| Аннотация / класс | Где используется | Что делает |
|---|---|---|
@ExceptionHandler |
метод | Обрабатывает исключения указанного типа. |
@ControllerAdvice |
класс | Глобальный advice для MVC: exceptions, global model attributes, binders. |
@RestControllerAdvice |
класс | @ControllerAdvice + @ResponseBody, удобно для JSON API. |
@ResponseStatus |
метод или exception class | Задает HTTP status. Для сложных REST-ошибок лучше ResponseEntity или ProblemDetail. |
ResponseStatusException |
throw из кода | Быстро пробросить HTTP status + reason без своего exception class. |
ProblemDetail |
return value/body | Стандартный формат HTTP API errors (application/problem+json). |
ResponseEntityExceptionHandler |
base class для advice | Удобная база для переопределения стандартных Spring MVC exception handlers. |
Пример с ProblemDetail:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleNotFound(UserNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("User not found");
problem.setDetail(ex.getMessage());
problem.setProperty("code", "USER_NOT_FOUND");
return problem;
}
}
17.5. Глобальная обработка исключений¶
Рекомендуемый подход для REST API:
1. Создать иерархию кастомных исключений: BusinessException, NotFoundException, ConflictException.
2. Создать единый @RestControllerAdvice.
3. Определить единый формат ошибок: свой DTO или стандартный ProblemDetail.
public record ErrorResponse(
String code,
String message,
List<String> details,
Instant timestamp
) {}
17.6. Возврат ошибок в HTML¶
@ControllerAdvice
public class HtmlExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ModelAndView handleAccessDenied(AccessDeniedException ex) {
ModelAndView mav = new ModelAndView("error/403");
mav.addObject("message", ex.getMessage());
return mav;
}
}
17.7. Возврат ошибок в JSON¶
@RestControllerAdvice
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class JsonExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(404)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
}
}
18. Interceptors¶
18.1. Что такое interceptor¶
HandlerInterceptor — компонент Spring MVC, который встраивается в цепочку обработки запроса.
В отличие от Servlet Filter (уровень контейнера) — interceptor живет внутри Spring MVC и имеет доступ к handler и ModelAndView.
18.2. preHandle¶
Выполняется до вызова контроллера.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
log.info("Request: {} {}", request.getMethod(), request.getRequestURI());
// Вернуть false — прервать обработку (ответ уже должен быть записан в response)
return true;
}
18.3. postHandle¶
Выполняется после контроллера, но до рендеринга view.
Не вызывается, если контроллер выбросил исключение до нормального return value.
При @ResponseBody / @RestController метод может быть вызван, но ModelAndView обычно будет null, потому что ответ формируется через HttpMessageConverter, а не через view.
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
modelAndView.addObject("globalData", globalService.getData());
}
}
18.4. afterCompletion¶
Выполняется после завершения запроса для interceptor'ов, чей preHandle успешно вернул true.
По смыслу похож на finally: сюда попадают и успешные запросы, и запросы с exception после прохождения interceptor'а.
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
long duration = System.currentTimeMillis() - startTime;
log.info("Request completed in {}ms, exception: {}", duration, ex);
// очистка ThreadLocal и т.д.
}
18.5. Для чего используются interceptors¶
- Логирование времени выполнения запроса.
- Проверка токена/сессии (light-weight auth, без Spring Security).
- Установка Locale.
- Передача глобальных данных во view (через
postHandle). - Аудит-логирование.
- Rate limiting (легковесный).
- MDC/трейсинг — добавление correlation ID в ThreadLocal.
18.6. Отличие interceptor от filter¶
| Servlet Filter | HandlerInterceptor | |
|---|---|---|
| Уровень | Servlet Container | Spring MVC |
| Когда | вокруг DispatcherServlet | внутри MVC-обработки |
| Доступ к handler | нет | да (знает метод контроллера) |
| Доступ к ModelAndView | нет | да (в postHandle) |
| Spring-бины | через ApplicationContext | да, сам является Spring-бином |
| Применение | CORS, encoding, security | логирование, auth, i18n, метрики |
Регистрация interceptor:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
19. Filters и место Spring MVC в servlet stack¶
19.1. Что такое servlet filter¶
Filter — компонент Jakarta Servlet API (jakarta.servlet.Filter), выполняющийся вокруг сервлетов.
@Component
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// до сервлета
chain.doFilter(request, response); // вызов следующего filter / сервлета
// после сервлета
}
}
19.2. Разница между Filter и Interceptor¶
| Filter | Interceptor | |
|---|---|---|
| Спецификация | Servlet API | Spring MVC |
| Уровень | До/вокруг DispatcherServlet | Внутри MVC (после DispatcherServlet) |
| Доступ к Spring Context | да, если filter зарегистрирован как Spring bean (@Component, FilterRegistrationBean, security chain); иначе нет |
да, является Spring-бином |
| Знает handler | нет | да |
| Может читать/изменять тело | да (оборачивая) | нет напрямую |
| Используется для | CORS, GZIP, security, encoding | логирование, auth, locale, метрики |
19.3. Где заканчивается servlet container и начинается Spring MVC¶
TCP Connection
└─► Servlet Container (Tomcat)
└─► Filter Chain (Servlet API)
└─► DispatcherServlet (Spring MVC начинается здесь)
└─► HandlerInterceptors
└─► Controller
19.4. Примеры использования filters¶
| Filter | Назначение |
|---|---|
CharacterEncodingFilter |
Устанавливает кодировку UTF-8 |
CorsFilter |
CORS-заголовки |
OncePerRequestFilter |
Базовый класс для filter, выполняющегося один раз |
| Spring Security Filter Chain | Вся цепочка безопасности |
CommonsRequestLoggingFilter |
Логирование запросов |
ShallowEtagHeaderFilter |
ETag для кеширования |