Перейти к содержанию

Исключения, 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 для кеширования