Spring MVC 中的 HandlerInterceptor

阅读更多
        在做 web 开发中,特别是使用 MVC 框架时,要是不谈谈拦截器这个概念,那可显示不出你的牛逼,o(∩_∩)o...哈哈!!!Struts2 中有拦截器,Spring MVC 同样也有拦截器。

        在 Spring MVC 中的 HandlerAdaptor 这篇文章中,我提到过,HandlerMapping 的 getHandler(request) 方法返回的并不是用于处理请求的 handler,而是被包装过的 HandlerExecutionChain:
package org.springframework.web.servlet;

public interface HandlerMapping {

    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
    
}

public class HandlerExecutionChain {

  private final Object handler;

  private HandlerInterceptor[] interceptors;

  private List interceptorList;

  /**
   * Create a new HandlerExecutionChain.
   * @param handler the handler object to execute
   */
  public HandlerExecutionChain(Object handler) {
    this(handler, null);
  }
  
  ......
  
}


        正如上述代码所示,HandlerExecutionChain 中除了封装了用于处理请求的 handler,同时还包含了 HandlerInterceptor。如果我们从 HandlerInterceptor 所处的位置溯源而上(HandlerInterceptor → HandlerExecutionChain → HandlerMapping),则会发现 HandlerMapping 是其最终的发源地。因此,我们只需要将 interceptor 注入我们定义的 HandlerMapping 中即可。

        老规矩,先看看 HandlerInterceptor 为何物:
package org.springframework.web.servlet;

public interface HandlerInterceptor {

  boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
      Object handler) throws Exception;
      
  void postHandle(
      HttpServletRequest request, HttpServletResponse response, Object handler, 
          ModelAndView modelAndView) throws Exception;
      
  void afterCompletion(
      HttpServletRequest request, HttpServletResponse response, Object handler, 
      Exception ex) throws Exception;
      
}

        我们自己写一个拦截器,一般实现 HandlerInterceptor 接口的 preHandle 方法即可。但我为了要写一个拦截器,即便后两个接口方法我不去实现,但也得写个空实现,煞是不爽。继承 java 的光荣传统,搞一个 XXXAdaptor 就可以了,这个 XXXAdaptor 实现的方法全部为空实现,我们自己的拦截器继承此 XXXAdaptor 即可,想覆写哪个方法就覆写哪个。当然,Spring NVC 已经为我们准备好了这个 adaptor:org.springframework.web.servlet.handler.HandlerInterceptorAdapter。此 adaptor 是一个抽象类。

        在学习 Spring MVC 的拦截器的概念时,我一直在想:为什么不把拦截器定义成 handler 的属性,各自的 handler 应用各自的 interceptor(s)?那你肯定会回答,定义上百个 handler,每个 handler 都注入 interceptor,累不累啊?肯定要定义在 HandlerMapping 里,使应用其 HandlerMapping 来映射的 handler 都应用这些拦截器(链)不就完了嘛。

        其实,我也不得不这样去想,但我们经常碰到的情况是:
  1. 在我们定义的 handler 中,都要检查 session 是否存在,这个拦截器定义在 HanlerMapping 中无可厚非。可是,总有那么几个 handler 在处理请求时不需要 session,怎么办?
  2. 即便我把那几个按非常规处理的 handler 剥离出来,不和按常规处理请求的 handler 用同一个 HandlerMapping,这几个 handler 用其它的 HandlerMapping 来做映射。OK,这样的确解决了,请继续往下看。
  3. 即便如此,再来怪一点的需求:这几个特殊的 handler 假若有 10 个,它们要分别应用 10 个不同的拦截器,咋搞?
  4. 再来个更怪异点的:即便对于一个 handler 而言,通过这个 url 过来的请求要用到 session 检查的拦截器;通过另外一个 url 过来的请求不做 session 检查,那不是傻眼了吗?

        暂且不管我为什么提出上述的问题,如果正如我一开始说的那样,拦截器是定义在 handler 里的,属于 handler 的属性,那么,上面的问题就不是问题了(最后一个问题仍然无法解决,当然,你可以认为这是个无理的需求,不合理的需求)。

        如果真的是一个 hanlder 就定义一个 interceptor(s),这是合理的软件设计理念吗?别忘了【二八原则】:花 20% 的精力去解决那 80% 的问题,往往很小的努力能解决很多类似的问题。那些特殊的问题也就不过 20%,尽管解决起来不那么容易,但毕竟出现这些问题的几率也不大。

        将 interceptor(s) 定义在 HandlerMapping 里,正是 Spring MVC 所采取的策略,利用这种方式,我们来说说如何解决上面我提出的问题。首先说明的是,一般情况下我们使用 ControllerClassNameHandlerMapping 这一个 HandlerMapping 就足够了,毕竟它是约定优于配置的体现,但为了解决那 20% 的问题,我不得不使用 SimpleUrlHandlerMapping。

        第一个问题,将那些特殊的 handler 不采用 ControllerClassNameHandlerMapping  而采用 SimpleUrlHandlerMapping 来映射。只不过我们写 url 和 handler 的映射关系时,可以用 ControllerClassNameHandlerMapping 式的 url 来做匹配,这样既没有丧失约定优于配置的优点,又可以实现特殊的 handler 应用特殊的拦截器(将这些特殊用到的拦截器定义在 SimpleUrlHandlerMapping 里即可)。

        第二/三个问题,我们整 10 个 SimpleUrlHandlerMapping 来一一映射这 10 个特殊应用的 handler,然后将 10 个不同的拦截器分别注入到这 10 个 SimpleUrlHandlerMapping 即可。

        第四个问题,同解决第二/三个问题采取的思路一样,为某一个 Controller 应用多个 HandlerMapping,url 的映射不存在关联关系,即某个 url 只可能映射到一个 HandlerMapping。若是还不明白这个案例是什么意思,那我就举一个具体的案例:NewsAction, news.action 这样的请求要应用 session 拦截器;news_static.action 用于生成静态文件,不要应用拦截器。映射时,一个为 news.action -> NewsAction,一个为 news_static.action -> NewsAction。如果采取模糊匹配,news.action 包含了 news*.action,怎么也不会映射到 news_static.action。把 news_static.action 的这个 HandlerMapping 优先级调高就行了,news.action 不匹配 news_static.action,自然会找下一个 HandlerMapping。甚至,你定义成 a.action 和 b.action 毫无关系的映射名来映射同一个 Controller 都行,这就是我说的 url 的映射不存在关联关系

        如此说来,Spring MVC 的 interceptor 机制,甚至都可以控制到 Controller 的方法级了,够细粒度了吧。还是那句话,发生这种情况的可能性只有 20%,也就是说,搞如此麻烦的配置的几率不大。这也终于解开了学习 interceptor 的结了。

        这里,再插一句话,为什么会提出那四个问题,是因为我一直在 struts2 的环境下开发,上述的问题的确在实际业务上碰到了。Struts2 可以为单个 action 定义拦截器(链),既而产生了我最开始的疑问。细细一想,Struts2 真的是基于类而定义的拦截器吗,或者说,Struts 解决 80% 的 Action 要使用的拦截器是怎么做的?回忆一下,使用 Struts2 时,经常使用的 url 是什么?是不是 /package/actionName.action,这个能不能理解成是 Struts2 的 HandlerMapping,只不过它只有一个 HandlerMapping 而已。其余的,大家可以自由发挥地去思考。另外,第四个问题,同一个 action 对不同的 url 映射应用不同的拦截器,采取新闻生成的案例,或许不恰当。因为在做静态生成时,一般不会再发 http 请求,现在都采用模板技术了。这里之所以拿这个案例来说,因为这是个历史遗留问题了而已。

        这篇文章旨在讲解原理,事实上,从 Spring 2.5 后,都是采用基于注解的 Controller 了, 采取 为某个 url 的请求单独应用拦截器,或者为所有 Controller 应用同样的拦截器。这种 的方式来定义更为简洁,是,简单归简单,毕竟这只是表现形式而已。但是,万变不离其宗,不变的永远是其背后的原理!

你可能感兴趣的:(Spring,MVC)