Filters, Interceptors и исключения¶
Три механизма вокруг контроллера, в порядке пути запроса: servlet-фильтры работают до DispatcherServlet,
interceptors - внутри MVC вокруг метода контроллера, обработчики исключений - когда что-то упало.
TCP-соединение
└─► Servlet Container (Tomcat)
└─► Filter Chain (Servlet API)
└─► DispatcherServlet ← здесь начинается Spring MVC
└─► HandlerInterceptors
└─► Controller
└─► HandlerExceptionResolver, если исключение
Filters¶
Filter (jakarta.servlet.Filter) - компонент Servlet API, который выполняется вокруг сервлета. Работает на
уровне servlet-контейнера, до DispatcherServlet, и не знает, какой контроллер будет вызван.
@Component
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// до сервлета
chain.doFilter(request, response); // следующий фильтр или сервлет
// после сервлета
}
}
Фильтр, объявленный Spring-бином (@Component или FilterRegistrationBean), Spring Boot сам регистрирует в
servlet-контейнере, и в нём доступны другие бины.
Типовые фильтры:
| Filter | Назначение |
|---|---|
CharacterEncodingFilter |
Кодировка UTF-8 |
CorsFilter |
CORS-заголовки |
OncePerRequestFilter |
Базовый класс: гарантирует один вызов на запрос |
| Цепочка Spring Security | Вся защита (см. Spring Security) |
CommonsRequestLoggingFilter |
Логирование запросов |
ShallowEtagHeaderFilter |
ETag для кеширования |
Interceptors¶
HandlerInterceptor - компонент Spring MVC, который встраивается в обработку запроса внутри
DispatcherServlet. В отличие от фильтра знает handler (метод контроллера) и видит ModelAndView. Вызывается в составе
HandlerExecutionChain.
preHandle - до вызова контроллера. Вернул false - обработка прерывается, ответ к этому моменту уже должен быть
записан.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute("startTime", System.currentTimeMillis());
log.info("Request: {} {}", request.getMethod(), request.getRequestURI());
return true;
}
postHandle - после контроллера, но до рендеринга view.
- не вызывается, если контроллер выбросил исключение
- при
@ResponseBodyи@RestControllerвызывается, ноModelAndViewобычноnull: ответ уже записалHttpMessageConverter
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
modelAndView.addObject("globalData", globalService.getData()); // общие данные во все view
}
}
afterCompletion - после завершения запроса, для interceptor'ов, чей preHandle вернул true. По смыслу как
finally: вызывается и при успехе, и при исключении.
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
log.info("Request completed in {}ms, exception: {}", System.currentTimeMillis() - startTime, ex);
// здесь же чистят ThreadLocal, MDC
}
Для чего используют:
- время выполнения запроса
- лёгкая проверка токена или сессии без Spring Security
- установка
Locale - общие данные во view через
postHandle - аудит
- лёгкий rate limiting
- MDC и трейсинг: correlation ID в
ThreadLocal
Регистрация:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
Filter и Interceptor¶
| Servlet Filter | HandlerInterceptor | |
|---|---|---|
| Спецификация | Servlet API | Spring MVC |
| Где | До и вокруг DispatcherServlet |
Внутри MVC, после DispatcherServlet |
| Знает handler | Нет | Да, знает метод контроллера |
Видит ModelAndView |
Нет | Да, в postHandle |
| Spring-бины | Да, если фильтр зарегистрирован как бин (@Component, FilterRegistrationBean, security chain) |
Да, сам бин |
| Может читать и менять тело | Да, через обёртку запроса или ответа | Не напрямую |
| Для чего | CORS, GZIP, безопасность, кодировка | Логирование, лёгкая авторизация, locale, метрики |
Итого: нужно до Spring MVC или с телом запроса - фильтр. Нужно знать, какой метод контроллера вызывается, или
трогать модель - interceptor. Ошибки контроллеров - @ControllerAdvice (ниже).
Обработка исключений¶
Исключения возникают при binding'е и валидации, в контроллере, в сервисном слое, при сериализации ответа.
DispatcherServlet перехватывает их и передаёт в HandlerExceptionResolver (см.
резолверы).
Note
Исключения из servlet-фильтров, в том числе из Spring Security, сюда не попадают: фильтры работают до
DispatcherServlet. Их переводит в ответ ExceptionTranslationFilter (см.
Spring Security).
@ExceptionHandler - метод-обработчик исключений указанного типа. Объявленный в контроллере, работает только для методов этого контроллера.
@Controller
public class UserController {
@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id); // бросает UserNotFoundException
}
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
}
Метод может вернуть:
- имя view или
ModelAndView- для HTML - DTO или
ProblemDetail- для REST ResponseEntity<?>- если нужно управлять статусом и заголовками
@ControllerAdvice - глобальный advice для всех контроллеров: обработчики исключений, общие атрибуты модели
(@ModelAttribute), настройка binder'ов (@InitBinder).
@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).
@RestControllerAdvice - @ControllerAdvice + @ResponseBody: все методы возвращают тело ответа, а не имя
view.
@RestControllerAdvice
public class GlobalRestExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
return new ErrorResponse("INTERNAL_ERROR", "Unexpected error occurred"); // детали наружу не отдаём
}
}
Обработка ошибок валидации (MethodArgumentNotValidException, HandlerMethodValidationException) - в
Валидации.
Аннотации и классы для ошибок:
| Аннотация или класс | Где | Что делает |
|---|---|---|
@ExceptionHandler |
Метод | Обрабатывает исключения указанного типа |
@ControllerAdvice |
Класс | Глобальный advice: исключения, общие атрибуты модели, binder'ы |
@RestControllerAdvice |
Класс | @ControllerAdvice + @ResponseBody, для JSON API |
@ResponseStatus |
Метод или класс исключения | Задаёт статус. Для сложных REST-ошибок лучше ResponseEntity или ProblemDetail |
ResponseStatusException |
throw из кода |
Быстро вернуть статус и причину без своего класса исключения |
ProblemDetail |
Результат или тело | Стандартный формат ошибок HTTP API, application/problem+json (RFC 9457) |
ResponseEntityExceptionHandler |
Базовый класс для advice | Готовые обработчики стандартных исключений Spring MVC, которые можно переопределить |
Ошибки в 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;
}
}
Ошибки в JSON¶
Подход для REST API:
- Иерархия своих исключений:
BusinessException,NotFoundException,ConflictException. - Один
@RestControllerAdviceна всё приложение. - Единый формат ошибок: свой DTO или стандартный
ProblemDetail(см. Единый формат ошибок).
Свой DTO:
public record ErrorResponse(
String code,
String message,
List<String> details,
Instant timestamp
) {}
@RestControllerAdvice
public class JsonExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage(), List.of(), Instant.now()));
}
}
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;
}
}
Note
В исходной версии на @RestControllerAdvice стояло @RequestMapping(produces = APPLICATION_JSON_VALUE). На
advice-классе эта аннотация ничего не делает: тип ответа определяет @ResponseBody и заголовок Accept.