• Tomcat 管道与阀


    Tomcat 管道与阀

     

    以下部分内容摘抄自《深入剖析Tomcat》,该书籍研究对象为tomcat4和tomcat5,下文中有些知识已经过时。

     

    tomcat4.1文档

    tomcat5.5文档

    tomcat7.0文档

     

    本章节是"深析Tomcat容器工作流程"的续文。如有不理解的地方,请先阅读上文。

     

     

    管道任务

    Pipeline接口

    Valve接口

    Contained接口

     

     

     

    管道任务

    该章节旨在说明当连接器调用了servlet容器的invoke()方法后会发生什么事。然后,在对应小节中讨论org.apache.catalina包中的4个相关接口,Pipleline、Valve、ValveContextConntained。

    管道包含该servlet容器将要调用的任务。一个阀表示一个具体的执行任务。在servlet容器的管道中,除了有一个基础阀,还可以添加任意数量的阀。阀的数量指的是额外添加的阀数量,即不包括基础阀。有意思的是,可以通过编辑Tomcat的配置文件(server.xml)来动态地添加阀。下图显示了一条管道及其阀。

    如果你对servlet编程中的过滤器有所了解的话,那么应该不难想像管道和阀的工作机制。管道就像过滤器链一样,而阀则好似是过滤器。阀与过滤器类似,可以处理传递给它的request对象和response对象。当一个阀执行完成后,会调用下一个阀继续执行。基础阀总是最后一个执行的

    一个servlet容器可以有一条管道。当调用了容器的invoke()方法后,容器将处理工作交由管道完成,而管道会调用其中的第一个阀开始处理。当第一个阀处理完后,它会调用后续的阀继续执行任务,直到管道中所有的阀都处理完成。下面是在管道的invoke()方法中执行的伪代码:

    但是,Tomcat4的设计者选择了另一种实现方法,通过引入接口org.apache.catalina.ValveContext来实现阀的遍历执行。下面是它的工作原理。

    当连接器调用容器的invoke()方法后,容器中要执行的任务并没有硬编码写在invoke()方法中,相反,容器会调用其管道的invoke()方法。Pipeline接口的invoke()方法的签名与Container接口的invoke()方法完全相同,如下所示:

    其中pipeline是该容器中的Pipeline接口的实例。【在Tomcat7中我没找到Pipeline的invoke方法】

    现在,管道必须保证添加到其中的所有阀及其基础阀都被调用一次,这是通过创建一个ValveContext接口实例来实现的【Tomcat7中已找不到该接口】。ValveContext是作为管道的一个内部类实现的,因此,ValveContext接口就可以访问管道的所有成员。ValveContext接口的最重要的方法是invokeNext():

     

    在创建了ValveContext实例后,管道会调用ValveContext实例的invokeNext()方法。ValveContext实例会首先调用管道中的第一个阀,第一个阀执行完后,会调用后面的阀继续执行。ValveContext实例会将自身传给每个阀,因此,每个阀都可以调用ValveContext实例的invokeNext()方法。下面是Valve接口的invoke()方法的签名:

     public void invoke(Request request, Response response) throws IOException, ServletException; 

    Valve接口的invoke()方法的实现类类似如下代码:

    org.apache.catalina.coreStandardPipeline类是所有servlet容器的中Pipeline接口的实现。在Tomcat4中,该类有一个实现了ValveContext接口的内部类,名为StandardPipelineValveContext。代码清单5-1给出了StandardPipelineValveContext类的定义。

     

    invokeNext()方法使用变量subscript和stage标明当前正在调用的阀。当第一次调用管道的invoke()方法时,subscript的值为0,stage的值为1,因此,第1个阀(数组索引为0)会被调用。管道中的第1个阀接收ValveContext实例,并调用其invokeNext()方法。这时,subscript的值变为1,这样就会调用第2个阀,此后以此类推。

    当从最后一个阀用invokeNext()方法时,subscript的值等于阀的数量,于是,基础阀被调用。

    Tomcat5才StandardPipeline类中移除了StandardPipelineValveContext类,却使用了org.apache.catalina.core.StandardValveContext类来调用阀,StandardValveContext类的定义在代码清单5-2中给出。

    你能看出Tomcat 4中StandardPipelineValveContext类和Tomcat 5中的StandardValveContext类的相似点吗?

     

    Tomact7中,早已移除了ValveContext,那在Tomcat7中是如何实现阀的遍历执行呢?在此我先找到了tomcat4、tomcat5和tomcat7版本的Valve、Pipeline接口:

    tomcat4 Valve

     

    tomcat4 Pipeline

     

    tomcat5 Valve

     

    tomcat5 Pipeline

     

    tomcat7 Valve。(Pipelinetomcat5一样)

    可以看到在tomcat5以后多提供了一个getNext方法,这个方法说明是:如果有的话,返回包含此阀门的管道中的下一个阀门。而且在tomcat5之后,Pipeline也多提供了一个方法getFirst,这个方法的说明是:返回已被区分为该管道的基本Valve的Valve实例(如果有)。

    Valve中加了getNext可以看出,这个getNext返回的依旧是一个Valve,这样的话,在同一个管道内,就可以先使用getFirst获取第一个阀并invoke执行它,在invoke内又可以getNext去获取下一个阀并执行,直到getNext返回值为空时,可以调用管道的getBasic方法,获取基础阀并invoke执行它。那只要在每个容器的基础阀中调用下一个容器管道的getFirst不就能遍历执行这些阀了嘛!比如说StandardEngineValve中先拿到它的管道,然后getFirst获取第一个阀,等非基础Valve执行完了,然后执行它的basic Valve,执行basic Valve时在调用Engine的子元素StandardHost中StandardHostValve的管道,然后使用StandardHostValve管道的getFisrt继续这样下去,所有的阀就得到执行了。

     

     

    现在,我们要详细介绍几个接口,包括Pipeline、ValveValveContext。此外,还会讨论一个阀类通常都会实现的接口org.apache.catalina.Contained。

     

     

    Pipeline接口

    Tomcat4中对于Pipeline接口,首先要提到的一个方法是invoke()方法,servlet容器调用invoke()方法来开始调用管道中的阀和基础阀。通过调用Pipeline接口的addValve()方法,可以向管道中添加新的阀,同样,也可以调用removeValve()方法从管道中删除某个阀。最后,调用setBasic()方法将基础阀门设置到管道中,调用其getBasic()方法则可以获取基础阀。基础阀是最后调用的阀,负责处理request对象及其对应的response对象。代码清单5-3给出了Pipeline接口的定义。

    Tomcat7中对于Pipeline接口,已经不在有invoke()方法,在容器初始化的时候创建好一个StandardPipeline

    由上图看出既然StandardPipeline实现了Lifecycle,则它必须管理自己的生命周期,这一点就和其他StandardContext,StandardEngine等一样,它们都要经历过NEW-...>STARTED。

    分别打开StandardEngine、StandardHost、StandardContext、和StandardWrapper这四个容器类,首先,执行一个类最先被执行的是构造方法,查看它们的构造方法(仅以StandardEngine为例,其他代码不在贴出):

     1 public StandardEngine() {
     2 
     3     super();
     4     pipeline.setBasic(new StandardEngineValve());
     5     /* Set the jmvRoute using the system property jvmRoute */
     6     try {
     7         setJvmRoute(System.getProperty("jvmRoute"));
     8     } catch(Exception ex) {
     9         log.warn(sm.getString("standardEngine.jvmRouteFail"));
    10     }
    11     // By default, the engine will hold the reloading thread
    12     backgroundProcessorDelay = 10;
    13 
    14 }

    可以看出它其中的pipeline把基础阀设置了进去,那这个pipeline从哪来的,什么时候被创建的?点进去查看pipeline可以看到它是父类ContainerBase里的一个protected属性,所以在创建StandardEngine时调用父类构造函数,此刻把这个pipeline属性也继承下来了。StandardHost、StandardContext、和StandardWrapper也都继承自ContainerBase,所以在创建它们的实例时,也都会创建一个自己的管道,并设置基础阀。

    在以StandardHost的startInternal为例(因为只有它在加入了其他阀):

     1 @Override
     2 protected synchronized void startInternal() throws LifecycleException {
     3 
     4     // 设置错误报告阀
     5     String errorValve = getErrorReportValveClass();
     6     if ((errorValve != null) && (!errorValve.equals(""))) {
     7         try {
     8             boolean found = false;
     9             Valve[] valves = getPipeline().getValves();
    10             for (Valve valve : valves) {
    11                 if (errorValve.equals(valve.getClass().getName())) {
    12                     found = true;
    13                     break;
    14                 }
    15             }
    16             if(!found) {
    17                 Valve valve =
    18                     (Valve) Class.forName(errorValve).getConstructor().newInstance();
    19                 getPipeline().addValve(valve);
    20             }
    21         } catch (Throwable t) {
    22             ExceptionUtils.handleThrowable(t);
    23             log.error(sm.getString(
    24                     "standardHost.invalidErrorReportValveClass",
    25                     errorValve), t);
    26         }
    27     }
    28     super.startInternal();
    29 }

    super.startInternal可以看到有一行代码:

    1 // 启动我们管道中的阀门(包括基本阀门)(如果有)
    2 if (pipeline instanceof Lifecycle) {
    3      // 不断getNext获取当前管道内的下一个阀,让它们经历NEW-...>STARTED
    4     ((Lifecycle) pipeline).start();
    5 }

    这行代码就是把所有的阀都状态都走一遍NEW-...>STARTED。到此管道和阀都准备好了。

    tomcat启动后,请求由Connector传递给Engine,也就是说到时候肯定会有一段代码调用了Engine的管道的getFirst把请求交给容器去处理,而这个请求要经历Engine、Host、Context、Wrapper这几个容器的管道,并执行阀里的invoke方法,接下来我们就去找是否有这段代码支持我们的猜测。

    我们把断点打在第一个容器的invoke()上,即找到StandardEngineValve的invoke()方法,在第一行打上断点,启动Bootstrap,然后访问http://localhost:8080/,此刻会进入断点:

    打开Debugger,从下自上依次点击Frames里每一个内容,先是run:

    1 @Override
    2 public void run() {
    3     if (target != null) {
    4         target.run();
    5     }
    6 }

    不用想,之前咱们就分析过,socket建立以后,对于每一个请求会创建一个独立的线程去处理,这个线程只不过是被tomcat封装了一下,在往上可以看到一个咱们之前分析过的对象ThreadPoolExecutor,这是tomcat创建的一个线程池,对于到来的socket连接请求,都会从这个池中获取一个线程去执行这个请求。在网上,JIOEndpoint,这个我也不说了就是之前的那个endpoint,我们重点看看当前它执行的方法是啥,是run方法的这一行:

    执行了一个handler.process,这个handle是Http11Protocol的一个内部类Http11ConnectionHandler的实例。在往上是AbstractProtocol$AbstractConnectionHandler内部类的方法process。在往上是AbstractHttp11Processor的process【关于tomcat连接器Connector连接器这一块我不是很熟,有时间在读一下这本书】,执行到这一行:

    这个adaptor,他就是CoyoteAdapter,之前我们也说过,给Http11Protocol做个适配的。在往上进入这个适配类的service方法,找到我们期盼的与肯定的代码:

    在往上不用说了,肯定是进入StandardEngineValve的invoke方法,管道和阀开始起作用。

     

    Valve接口

    Tomcat4中阀是Valve接口的实例,用来处理接收到的请求。该接口有两个方法,invoke()方法和getInfo()方法。invoke()方法已经在前面讨论过了,getInfo()方法返回阀的实现信息。代码清单5-4给出了Valve接口的定义。

    Tomcat7中上面管道章节中,我们也跟踪源码了,为什么tomcat会有管道和阀,而不是直接把请求交给StandardEngine,我想这也不必我多说,你心里多少也有些想法,有了管道和阀可以不断的对该管道中的阀进行调用,相当于一条链上的多个Filter一样,通过管道和阀去控制、设置请求和响应。

    每个标准实现类都有一个标准实现类的阀,比如StandardEngine有StandardEngineValve,StandardHost有StandardHostValve。每个阀里的invoke各有特色,再次不在多说。

     

     

    Contained接口

    Tomcat4中阀可以选择是否实现org.apache.catalina.Contained接口,该接口的实现类可以通过接口中的方法至多与一个servlet容器相关联。代码清单5-6给出了Contained接口的定义。

    前进时,请别遗忘了身后的脚印。
  • 相关阅读:
    python 文件目录/方法
    python文件
    python模块
    python数据结构
    python函数
    python迭代器和生成器
    python循环语句
    python控制语句 if
    python数字
    个人课程总结
  • 原文地址:https://www.cnblogs.com/liudaihuablogs/p/14167353.html
Copyright © 2020-2023  润新知