`
beyondyuefei
  • 浏览: 12765 次
  • 性别: Icon_minigender_1
  • 来自: 福州
文章分类
社区版块
存档分类
最新评论

浅谈Struts2拦截器Interceptor的设计原理

 
阅读更多

使用过struts2框架的朋友们都知道 拦截器Interceptor 在struts2中的地位是非常重要的,可以说是struts2在控制流调度部分的核心,并且struts2做为一个MVC框架,之所以在 controller层有如此强大的扩展能力,完全是由其拦截器的设计决定的,我们先看看下面这张图:

从上图中我们可以看到,整个 Action(Controller)层被一层层的Interceptor包裹了起来,而Action则处于最核心的位置,整体形成了一个责任链调用模式,那么struts2为什么采取如此的设计呢?这就不得不提一下整个struts2框架的设计理念。我们采用的是面向对象程序设计,那么我们知道一个对象可以从某个角度上分为有状态对象和无状态对象,而区分的标准就是对象是否具备代表其自身属性的字段,而Web层的http协议是基于 请求-响应 机制的,其核心处理过程便是 “请求参数-->逻辑处理--->返回参数”。那么在对象中如何来实现请求-响应的过程呢? 我们可以来横向对比 servlet 标准和spring mvc 的实现方式,他们都是采用JAVA中的天然语法,即对象的方法来实现的,请求参数都是自然的包含在了方法的参数中,servlet标准是直接把web容器中的request和respose对象作为 service 方法的参数,而spring mvc则是对方法参数进行额外的绑定处理,在返回值的处理上他们也有所不同,但是 方法作为一个载体天然的把 请求参数、业务逻辑处理和返回值封装在方法内部了。回到我们现在要讨论的 struts2上来,struts2对于http请求-响应机制的实现方式与前两者完全不同,对于请求参数和返回参数,struts2是用对象的属性字段承载,而业务逻辑处理则是用的无参数的方法来响应。

可以这么认为,servlet标准和spring mvc 采用的是 无状态对象,而struts2是采用的有状态对象作为请求响应的载体,这是他们在设计理念上的根本区别!servlet标准采用无状态的 servlet对象是因为 传统的servlet不是线程安全的,即 servlet对象是单例的,所以直接把 原生的 web容器对象 request和response作为参数传递给 servlet 的service方法 以避开线程安全问题,这也造成了servlet对象与web容器的紧耦合。而struts2则是另辟蹊径,在整体设计上 通过 ThreadLocal 将请求参数绑定到线程上下文中去,使得 Action完全和web容器解耦,同时也解决了线程安全问题,因此, Action对象作为一个普通的 POJO ,其 属性字段便成为了 请求参数和返回参数寄居的理想场所,无参的方法处理业务逻辑。

既然Action是一个有状态的对象,并且请求参数是以对象属性的形式出现,那么我们在设计的时候如何能够将请求参数注入到 Action对象中,并且能在 Action层设计一个可扩展的方案呢?这里就引入了 AOP 的设计理念,struts2将一个或一组 Action对象作为AOP中的拦截点,众多的 Interceptor 作为 切面环绕在 Action的周围。我们打开struts2自带的核心配置文件 struts-default.xml ,里面定义了众多 struts2默认的Interpector,其中就包含了注入Action对象属性值的 ParamterInterpector ,还有 fileuploadInterceptor 等等好多,加上我们自己在应用的 struts.xml 中定义的 Inteceptor,这些拦截器就形成了一个 AOP的拦截器链。Action中的方法负责执行核心业务逻辑,而这些Interceptor则扮演的辅助的角色,这样整个Action层就得到了极大的扩展。接下来我们看看struts2源码中是如何实现这个AOP效果的。

我们先看下 Interceptor接口的定义:

public interface Interceptor extends Serializable {

    /**
     * Called to let an interceptor clean up any resources it has allocated.
     */
    void destroy();

    /**
     * Called after an interceptor is created, but before any requests are processed using
     * {@link #intercept(com.opensymphony.xwork2.ActionInvocation) intercept} , giving
     * the Interceptor a chance to initialize any needed resources.
     */
    void init();

    /**
     * Allows the Interceptor to do some processing on the request before and/or after the rest of the processing of the
     * request by the {@link ActionInvocation} or to short-circuit the processing and just return a String return code.
     *
     * @param invocation the action invocation
     * @return the return code, either returned from {@link ActionInvocation#invoke()}, or from the interceptor itself.
     * @throws Exception any system-level error, as defined in {@link com.opensymphony.xwork2.Action#execute()}.
     */
    String intercept(ActionInvocation invocation) throws Exception;

}


其核心方法便是这个 intercept(ActionInvocation invocation) 方法,具体的Interceptor实现类重写该方法。其中的方法参数 ActionInvocation 代表的是 struts2 中 (严格来说是struts2的 xwork部分)的核心调度元素,他负责整个 intercepter、action、result 的调度,具体的实现类为 defaultActionInvocation:

public class DefaultActionInvocation implements ActionInvocation {
protected Iterator<InterceptorMapping> interceptors;
/*此处省略 N 多代码*/
public String invoke() throws Exception {
        String profileKey = "invoke: ";
        try {
            UtilTimerStack.push(profileKey);

            if (executed) {
                throw new IllegalStateException("Action has already executed");
            }

            if (interceptors.hasNext()) {
                final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
                String interceptorMsg = "interceptor: " + interceptor.getName();
                UtilTimerStack.push(interceptorMsg);
                try {
                                resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
                            }
                finally {
                    UtilTimerStack.pop(interceptorMsg);
                }
            } else {
                resultCode = invokeActionOnly();
            }
/*此处省略 N 多代码*/


我们来看看invocation对Interceptor的调度,首先 众多定义好的 Interceptor很自然的存储在 链表的数据结构当中,只不过在 DefaultInvocation当中 Interceptor已经被包装为 InterceptorMapping对象当中,并且链表已经被转换为 Iterator 迭代器的形式 ,为接下来的 Interceptor调用做好了准备。也许很多人会认为接下来对Interpector的调用无非就是一个简单的循环遍历,然后逐个调用 Interceptor的 intercept方法,最后调用 Action的方法嘛,如下:

        while(interceptors.hasNext()) {
            	  final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
            	  resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
            }
        resultCode = invokeActionOnly();

这样做是行不通的,首先 这样直接循环遍历 无法实现 AOP的效果,即在执行 invokeActionOnly() 之前执行一段逻辑,而在执行Action之后再执行另一段逻辑,即 before() 和 after() 通知;再者 无法灵活的改变 程序执行流程。现在我们就来看看struts2中是怎么做的,回过头我们再看下 defaultActionInvocation 中的关键代码:

           if (interceptors.hasNext()) {
                final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
                String interceptorMsg = "interceptor: " + interceptor.getName();
                UtilTimerStack.push(interceptorMsg);
                try {
                                resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
                            }
                finally {
                    UtilTimerStack.pop(interceptorMsg);
                }
            } else {
                resultCode = invokeActionOnly();
            }


从上面的代码中我们至少可以得出两点:1. 没有看到Iterator迭代器对所有Interceptor的循环遍历,而只是在 if 块中调用了第一个Interceptor的intercept方法; 2. 调用action 的代码 invokeActionOnly 方法 和 Interceptor 处于 if-else 的互斥代码块中。

针对以上两个问题 我们必须结合 Interceptor的实现类 来给出解释,来看看我们自己定的一个Interceptor实现类吧:

   public class Interceptor1 extends AbstractInterceptor {
	
	public String intercept(ActionInvocation invocation) throws Exception {
		// AOP中的 before通知
		System.out.println("do something before action invoked");
		// 这里实现了Interceptor的递归调用
		String resultCode = invocation.invoke();
		// AOP中的 after通知
		System.out.println("do something before action invoked");
		return resultCode;
	}

    }


我们看到,负责核心调度的 DefaultActionInvocation 对象将其自身作为参数传递给 Interceptor 实现类,而在 Interceptor的实现类中又通过 invocation 参数回调了 DefaultActionInvocation 负责核心调度任务的 invoke() 方法。这样一来一去 产生的效果如下图:


此图是从struts2的 Apache官网上截取的一部分,我们可以看到,Interceptor链与Action共同组成了一个类似堆栈的数据结构,而Action对象则处于栈底,而对这个堆栈的遍历过程则是由堆栈中的元素自己来完成的,即前一个Interceptor负责调用下一个Interceptor,以此类推,最后一个Interceptor则负责调用处于栈底的Action对象,这是一个递归的调用过程。因此,在Interceptor实现类中,处于 invocation.invoke() 之前的代码(AOP中的before通知)按照顺序依次被调度执行,处于 invocation.invoke() 之后的代码(AOP中的after通知)则暂时被保存了起来,当最后一个 Interceptor 调用了 invocation.invoke() 操作时,触发了 defaultActionInvocation 中 invoke()方法内 if-else 互斥块的 else 区域:

              else {
                resultCode = invokeActionOnly();
            }


这里即执行了 AOP拦截点 Action对象的调用,当 Action对象调用执行完毕后,由于递归的特性,之前暂时保存起来的诸多 Interceptor 中 的 after通知,即位于 invocation.invoke() 之后的代码被依次逆序执行,这样便实现了一个 AOP的调用效果!

所谓的AOP,在struts2中被精巧的实现了,采用的只是最普通的 数据结构 + 算法 (递归)。 而且这里的 AOP切面并不是一个而是由一组Interceptor形成的责任链调用模式,在传递调用的过程中当其中的某一个Interceptor没有再调用 invocation.invoke()进行回调,而是直接返回 resultCode ,那么整个调用过程便提前逆序执行,即后面的Interceptor和栈底的Action不会再被调用执行了,这可以让我们在Interpector中做一些诸如校验工作的时候由于某些原因而让请求调度过程中止退出。

由于 Interceptor可以很方便的对被拦截的Action对象的属性进行操作,就像struts2 自带的 ParamterInterceptor 做的那样对 Action对象的属性进行注入,因此我们可以认为 struts2 中的 IOC容器和Interceptor的AOP调用模式共同完成了对Action对象的依赖注入操作!(struts2中的IOC容器可以参见我在CSDN中的上一篇博客),当然,如果将Action对象托管给 spring 的 IOC容器,那么 spring IOC 也参与了Action对象的依赖注入了。

对比Tomcat过滤器(Filter):

相对于struts2中的拦截器Interceptor在struts2设计中的重要程度而言,Tomcat的过滤器Filter显得低调不少,因为在整个 Tomcat的设计核心是连接器 Connector和容器Container 这两大模块,Connector模块负责建立ServerSocket监听请求,分配线程池中的线程进行处理,并且将 原始的 Http 请求报文内容解析为 Request 和 Respose,然后将控制权转交到 Container模块中去处理了,而 Container 作为抽象的容器,又分别由 Engine、Host、Context和Wrapper 四大具体的容器来实现,这4大容器之间的父子关系,及生命周期的管理,request和response在管道中的传递等才是tomcat设计的重点之一,而 Filter 只是在 最后一子的子容器 Wrapper 中被使用到,因为 Wrapper对应一个 Servlet 的调用,所以在调用 Servlet的方法前 必须先经过它所关联的过滤器链,其调用方式和 struts2 中的 Interceptor十分类似,都是递归!

final class ApplicationFilterChain implements FilterChain, CometFilterChain {
   /**
     * Filters.
     */
    private ApplicationFilterConfig[] filters = 
        new ApplicationFilterConfig[0];


    /**
     * The int which is used to maintain the current position 
     * in the filter chain.
     */
    private int pos = 0;


    /**
     * The int which gives the current number of filters in the chain.
     */
    private int n = 0;


    /**
     * The servlet instance to be executed by this chain.
     */
    private Servlet servlet = null;

  private void internalDoFilter(ServletRequest request, 
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            Filter filter = null;
            filter = filterConfig.getFilter();
            filter.doFilter(request, response, this);
            return;
          }
       // We fell off the end of the chain -- call the servlet instance
      servlet.service(request, response);
}


上面的代码我省略了很多,只贴出核心逻辑部分。可以看出 tomcat是将 过滤器链 封装在 一个 数组中,而 if 代码块 判断的便是 是否 到达数组底部了,类似于 struts2中的 iterator.hasNext() , 关键的一行在于 :filter.doFilter(request, response, this); 这里便是一个分水岭,类似于 struts2中的 invocation.invoke() ,而在我们自定义的 Filter 中通过回调 chain.doFilter(request,response) 方法 最终又会嵌套调用 internalDoFilter 方法,这样便形成了 递归,而AOP的拦截点就是 servlet中的方法。具体不再详细展开了,总之其算法思想和 struts2 的 Interceptor十分类似。

对比 Spring MVC 中的拦截器 Interceptor:

Spring mvc 同样也有自身的 拦截器设计,这里的 拦截器和 Spring 本身的 AOP拦截是两码事,完全不同的实现机制,后者是通过 JDK 的动态代理 或者 第三方的字节码生成机制实现的,而 Spring mvc 中则 没那么复杂。

对比 struts2的拦截器和tomcat 的过滤器实现算法, spring mvc则是用的最简单的循环调用来达到 对Controller方法的前后拦截,之所以循环能实现,是因为它的拦截器接口设计和 struts2中的 拦截器接口不同,我们来看看 spring mvc中的拦截器接口源码:

public interface HandlerInterceptor {

	/**
	 * Intercept the execution of a handler. Called after HandlerMapping determined
	 * an appropriate handler object, but before HandlerAdapter invokes the handler.
	 * <p>DispatcherServlet processes a handler in an execution chain, consisting
	 * of any number of interceptors, with the handler itself at the end.
	 * With this method, each interceptor can decide to abort the execution chain,
	 * typically sending a HTTP error or writing a custom response.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler chosen handler to execute, for type and/or instance evaluation
	 * @return <code>true</code> if the execution chain should proceed with the
	 * next interceptor or the handler itself. Else, DispatcherServlet assumes
	 * that this interceptor has already dealt with the response itself.
	 * @throws Exception in case of errors
	 */
	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
	    throws Exception;

	/**
	 * Intercept the execution of a handler. Called after HandlerAdapter actually
	 * invoked the handler, but before the DispatcherServlet renders the view.
	 * Can expose additional model objects to the view via the given ModelAndView.
	 * <p>DispatcherServlet processes a handler in an execution chain, consisting
	 * of any number of interceptors, with the handler itself at the end.
	 * With this method, each interceptor can post-process an execution,
	 * getting applied in inverse order of the execution chain.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler chosen handler to execute, for type and/or instance examination
	 * @param modelAndView the <code>ModelAndView</code> that the handler returned
	 * (can also be <code>null</code>)
	 * @throws Exception in case of errors
	 */
	void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;

	/**
	 * Callback after completion of request processing, that is, after rendering
	 * the view. Will be called on any outcome of handler execution, thus allows
	 * for proper resource cleanup.
	 * <p>Note: Will only be called if this interceptor's <code>preHandle</code>
	 * method has successfully completed and returned <code>true</code>!
	 * <p>As with the {@code postHandle} method, the method will be invoked on each
	 * interceptor in the chain in reverse order, so the first interceptor will be
	 * the last to be invoked.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler chosen handler to execute, for type and/or instance examination
	 * @param ex exception thrown on handler execution, if any
	 * @throws Exception in case of errors
	 */
	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

}


相比 Struts2的Interceptor接口中只有一个 intercept(Invocation invocation) 方法声明,spring mvc中则有 3个,从名字就可以看的出来 第一个方法是负责拦截点执行之前的代码逻辑,第二个方法是负责拦截点执行之后的代码逻辑,最后一个方法是负责渲染视图之前的代码逻辑,那么我们可以很自然的想象这里肯定是用 两个循环遍历Interceptor来实现前、后拦截,翻开源码一看,确实如此:

 protected   void  doDispatch( final  HttpServletRequest request, HttpServletResponse response)  throws  Exception {    
    HttpServletRequest processedRequest = request;    
     //这是从handlerMapping中得到的执行链    
     HandlerExecutionChain mappedHandler =  null ;    
     int  interceptorIndex = - 1 ;    
        
     ........    
      try  {    
          //我们熟悉的ModelAndView开始出现了。    
         ModelAndView mv =  null ;    
          try  {    
             processedRequest = checkMultipart(request);    
    
              // 这是我们得到handler的过程    
             mappedHandler = getHandler(processedRequest,  false );    
              if  (mappedHandler ==  null  || mappedHandler.getHandler() ==  null ) {   
                 noHandlerFound(processedRequest, response);    
                  return ;    
             }    
   
              // 这里取出执行链中的 Interceptor进行前处理    
              if  (mappedHandler.getInterceptors() !=  null ) {    
                  for  ( int  i =  0 ; i < mappedHandler.getInterceptors().length; i++) {   
                      HandlerInterceptor interceptor = mappedHandler.getInterceptors()[i];   
                      if  (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {   
                          triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response,  null );   
                           return ;    
                     }    
                     interceptorIndex = i;    
                 }    
             }    
   
              //1. 在执行handler之前,用 HandlerAdapter先检查一下handler的合法性:是不是按Spring的要求编写的。   
              HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());   
                2.  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());   
     
              //3. 这里取出执行链中的 Interceptor进行后处理    
              if  (mappedHandler.getInterceptors() !=  null ) {    
                 for  ( int  i = mappedHandler.getInterceptors().length -  1 ; i >=  0 ; i--) {   
                      HandlerInterceptor interceptor = mappedHandler.getInterceptors()[i];   
                    interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);   
                  }    
             }    
         }    
            
         ........    
    
          // Did the handler return a view to render?    
         //这里对视图生成进行处理    
          if  (mv !=  null  && !mv.wasCleared()) {    
             render(mv, processedRequest, response);    
         }    
         .......    
 }   


其中 的 1、2、3 这三处标出说明了这个处理过程,第 2点标注处便是执行了 Controller的方法了。需要注意的是 HandlerInterceptor 接口中的 preHandler 方法声明,该方法的返回值为 boolean 类型,参考注释及代码我们可以知道这里是通过一个标志位来判断拦截器链是否继续往下执行,和 struts2中的拦截器及tomcat过滤器有所不同,这也是因为这里用的是循环,而不是递归!

至于是递归好还是循环好 这里我觉得仁者见仁了,递归方法实现的比较精妙,而循环实现的方式比较浅显易懂。两种不同的实现算法也导致接口设计的不同,如:spring mvc和struts2中的拦截器接口。

最后我想总结的是: Struts2中的拦截器拦截的对象是整个 Action ,这是由 Struts2的设计理念决定的,负责请求-响应类的是 一个有状态的POJO。 而 Filter和 Srping mvc中的过滤器拦截的对象是某个方法,负责请求响应类的是一个 无状态对象中的某个方法。也正式由于这些原因,使得虽然 Filter 和 spring mvc的 Interceptor 在平时也用到一些,但是在对 Controller层的扩展能力上 显然比 Struts2逊色的多,毕竟,拦截一个方法,然后操作方法中的参数 比 拦截一个 POJO,直接 geter setter 该对象的属性来得复杂的多!这也是 Struts2中的拦截器被应用的如此广泛的重要原因!

看来在 Controller层上的设计理念的差异决定了两种MVC框架的不同走向,站在比 框架 更高一层的角度 去学习和总结他们的原理,今后再有其他神马 MVC 框架出来,也都只是浮云而已。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics