使用Interceptor


上图虚线框就是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. Render
  12. └────────────┘
  13. ModelAndView
  14. └────────────┘
  15. ┌───────────┐
  16. ├─┼─>│Controller1│────┤
  17. └───────────┘
  18. ┌───────────┐
  19. └─┼─>│Controller2│────┘
  20. └───────────┘

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

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

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

  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. }
  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方法调用前执行,是Controller方法正常返回后执行,而afterCompletion()无论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. };
  11. }

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

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

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

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

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

使用Interceptor - 图2下载练习: (推荐使用IDE练习插件快速下载)

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