上图虚线框就是Filter2的拦截范围,Filter组件实际上并不知道后续内部处理是通过Spring MVC提供的还是其他Servlet组件,因为Filter是Servlet规范定义的标准组件,它可以应用在任何基于Servlet的程序中。

    如果只基于Spring MVC开发应用程序,还可以使用Spring MVC提供的一种功能类似Filter的拦截器:Interceptor。和Filter相比,Interceptor拦截范围不是后续整个处理流程,而是仅针对Controller拦截:

    1. ┌───────┐
    2. Filter1
    3. └───────┘
    4. ┌───────┐
    5. Filter2
    6. └───────┘
    7. ┌─────────────────┐
    8. DispatcherServlet│<───┐
    9. └─────────────────┘
    10. ┌────────────┐
    11. ModelAndView
    12. └────────────┘
    13. ├─┼─>│Controller1│──┼─┤
    14. └───────────┘
    15. ┌───────────┐
    16. └─┼─>│Controller2│──┼─┘
    17. └───────────┘

    上图虚线框就是Interceptor的拦截范围,注意到Controller的处理方法一般都类似这样:

    所以,Interceptor的拦截范围其实就是Controller方法,它实际上就相当于基于AOP的方法拦截。因为Interceptor只拦截Controller方法,所以要注意,返回ModelAndView后,后续对View的渲染就脱离了Interceptor的拦截范围。

    使用Interceptor的好处是Interceptor本身是Spring管理的Bean,因此注入任意Bean都非常简单。此外,可以应用多个Interceptor,并通过简单的@Order指定顺序。我们先写一个LoggerInterceptor

    1. @Order(1)
    2. @Component
    3. public class LoggerInterceptor implements HandlerInterceptor {
    4. final Logger logger = LoggerFactory.getLogger(getClass());
    5. @Override
    6. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    7. logger.info("preHandle {}...", request.getRequestURI());
    8. if (request.getParameter("debug") != null) {
    9. PrintWriter pw = response.getWriter();
    10. pw.write("<p>DEBUG MODE</p>");
    11. pw.flush();
    12. return false;
    13. }
    14. return true;
    15. @Override
    16. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    17. logger.info("postHandle {}.", request.getRequestURI());
    18. if (modelAndView != null) {
    19. modelAndView.addObject("__time__", LocalDateTime.now());
    20. }
    21. }
    22. @Override
    23. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    24. logger.info("afterCompletion {}: exception = {}", request.getRequestURI(), ex);
    25. }
    26. }

    一个Interceptor必须实现HandlerInterceptor接口,可以选择实现preHandle()postHandle()afterCompletion()方法。preHandle()是Controller方法调用前执行,postHandle()是Controller方法正常返回后执行,而无论Controller方法是否抛异常都会执行,参数ex就是Controller方法抛出的异常(未抛出异常是null)。

    我们再继续添加一个AuthInterceptor,用于替代上一节使用AuthFilter进行Basic认证的功能:

    这个AuthInterceptor是由Spring容器直接管理的,因此注入UserService非常方便。

    最后,要让拦截器生效,我们在WebMvcConfigurer中注册所有的Interceptor:

    1. @Bean
    2. WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) {
    3. return new WebMvcConfigurer() {
    4. public void addInterceptors(InterceptorRegistry registry) {
    5. for (var interceptor : interceptors) {
    6. registry.addInterceptor(interceptor);
    7. }
    8. }
    9. ...
    10. }

    如果拦截器没有生效,请检查是否忘了在WebMvcConfigurer中注册。

    在Controller中,Spring MVC还允许定义基于@ExceptionHandler注解的异常处理方法。我们来看具体的示例代码:

    异常处理方法没有固定的方法签名,可以传入ExceptionHttpServletRequest等,返回值可以是void,也可以是ModelAndView,上述代码通过@ExceptionHandler(RuntimeException.class)表示当发生RuntimeException的时候,就自动调用此方法处理。

    可以编写多个错误处理方法,每个方法针对特定的异常。例如,处理LoginException使得页面可以自动跳转到登录页。

    使用ExceptionHandler时,要注意它仅作用于当前的Controller,即ControllerA中定义的一个方法对ControllerB不起作用。如果我们有很多Controller,每个Controller都需要处理一些通用的异常,例如LoginException,思考一下应该怎么避免重复代码?

    从下载练习:使用Interceptor (推荐使用快速下载)

    Spring MVC提供了Interceptor组件来拦截Controller方法,使用时要注意Interceptor的作用范围。

    使用Interceptor - 图1