阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用Interceptor

23次阅读
没有评论

共计 5244 个字符,预计需要花费 14 分钟才能阅读完成。

在 Web 程序中,注意到使用 Filter 的时候,Filter 由 Servlet 容器管理,它在 Spring MVC 的 Web 应用程序中作用范围如下:

         │   ▲
         ▼   │
       ┌───────┐
       │Filter1│
       └───────┘
         │   ▲
         ▼   │
       ┌───────┐
┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐
       └───────┘
│        │   ▲                  │
         ▼   │
│ ┌─────────────────┐           │
  │DispatcherServlet│◀───┐
│ └─────────────────┘    │      │
   │              ┌────────────┐
│  │              │ModelAndView││
   │              └────────────┘
│  │                     ▲      │
   │    ┌───────────┐    │
│  ├───▶│Controller1│────┤      │
   │    └───────────┘    │
│  │                     │      │
   │    ┌───────────┐    │
│  └───▶│Controller2│────┘      │
        └───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

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

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

       │   ▲
       ▼   │
     ┌───────┐
     │Filter1│
     └───────┘
       │   ▲
       ▼   │
     ┌───────┐
     │Filter2│
     └───────┘
       │   ▲
       ▼   │
┌─────────────────┐
│DispatcherServlet│◀───┐
└─────────────────┘    │
 │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ┐
 │                     │
 │ │            ┌────────────┐ │
 │              │   Render   │
 │ │            └────────────┘ │
 │                     ▲
 │ │                   │       │
 │              ┌────────────┐
 │ │            │ModelAndView│ │
 │              └────────────┘
 │ │                   ▲       │
 │    ┌───────────┐    │
 ├─┼─▶│Controller1│────┤       │
 │    └───────────┘    │
 │ │                   │       │
 │    ┌───────────┐    │
 └─┼─▶│Controller2│────┘       │
      └───────────┘
   └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

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

@Controller
public class Controller1 {@GetMapping("/path/to/hello")
    ModelAndView hello() {...}
}

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

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

@Order(1)
@Component
public class LoggerInterceptor implements HandlerInterceptor {final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.info("preHandle {}...", request.getRequestURI());
        if (request.getParameter("debug") != null) {PrintWriter pw = response.getWriter();
            pw.write("<p>DEBUG MODE</p>");
            pw.flush();
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.info("postHandle {}.", request.getRequestURI());
        if (modelAndView != null) {modelAndView.addObject("__time__", LocalDateTime.now());
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.info("afterCompletion {}: exception = {}", request.getRequestURI(), ex);
    }
}

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

preHandle() 中,也可以直接处理响应,然后返回 false 表示无需调用 Controller 方法继续处理了,通常在认证或者安全检查失败时直接返回错误响应。在 postHandle() 中,因为捕获了 Controller 方法返回的 ModelAndView,所以可以继续往ModelAndView 里添加一些通用数据,很多页面需要的全局数据如 Copyright 信息等都可以放到这里,无需在每个 Controller 方法中重复添加。

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

@Order(2)
@Component
public class AuthInterceptor implements HandlerInterceptor {final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {logger.info("pre authenticate {}...", request.getRequestURI());
        try {authenticateByHeader(request);
        } catch (RuntimeException e) {logger.warn("login by authorization header failed.", e);
        }
        return true;
    }

    private void authenticateByHeader(HttpServletRequest req) {String authHeader = req.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Basic")) {logger.info("try authenticate by authorization header...");
            String up = new String(Base64.getDecoder().decode(authHeader.substring(6)), StandardCharsets.UTF_8);
            int pos = up.indexOf(':');
            if (pos > 0) {String email = URLDecoder.decode(up.substring(0, pos), StandardCharsets.UTF_8);
                String password = URLDecoder.decode(up.substring(pos + 1), StandardCharsets.UTF_8);
                User user = userService.signin(email, password);
                req.getSession().setAttribute(UserController.KEY_USER, user);
                logger.info("user {} login by authorization header ok.", email);
            }
        }
    }
}

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

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

@Bean
WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) {return new WebMvcConfigurer() {public void addInterceptors(InterceptorRegistry registry) {for (var interceptor : interceptors) {registry.addInterceptor(interceptor);
            }
        }
        ...
    };
}

注意

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

处理异常

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

@Controller
public class UserController {@ExceptionHandler(RuntimeException.class)
    public ModelAndView handleUnknowException(Exception ex) {return new ModelAndView("500.html", Map.of("error", ex.getClass().getSimpleName(), "message", ex.getMessage()));
    }
    ...
}

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

注意到我们返回了一个新的ModelAndView,这样在应用程序内部如果发生了预料之外的异常,可以给用户显示一个出错页面,而不是简单的 500 Internal Server Error 或 404 Not Found。例如 B 站的错误页:

使用 Interceptor

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

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

练习

使用 Interceptor。

下载练习

小结

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

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计5244字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中