Tomcat 管道与阀

Tomcat 管道与阀

 Tomcat 管道与阀

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

 

tomcat4.1文档

tomcat5.5文档

tomcat7.0文档

 

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

 

 

管道任务

Pipeline接口

Valve接口

Contained接口

 

 

 

管道任务

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

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

Tomcat 管道与阀

 

 

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

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

Tomcat 管道与阀

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

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

Tomcat 管道与阀

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

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

Tomcat 管道与阀

 

 

 

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

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

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

Tomcat 管道与阀

 

 

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

Tomcat 管道与阀

 

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 管道与阀

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

 

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

Tomcat 管道与阀

tomcat4 Valve

 

Tomcat 管道与阀

tomcat4 Pipeline

 

Tomcat 管道与阀

tomcat5 Valve

 

Tomcat 管道与阀

tomcat5 Pipeline

 

Tomcat 管道与阀

tomcat7 Valve。(Pipeline和tomcat5一样)

可以看到在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、Valve和ValveContext。此外,还会讨论一个阀类通常都会实现的接口org.apache.catalina.Contained。

 

 

Pipeline接口

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

Tomcat 管道与阀

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

Tomcat 管道与阀

由上图看出既然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/,此刻会进入断点:

Tomcat 管道与阀

 

 

打开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方法的这一行:

Tomcat 管道与阀

 

 

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

Tomcat 管道与阀

 

 

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

Tomcat 管道与阀

 

 

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

 

Valve接口

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

Tomcat 管道与阀

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

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

 

 

Contained接口

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

Tomcat 管道与阀

 

 

Tomcat 管道与阀

上一篇:Tomcat中的Valve以及自定义Valve


下一篇:《SteamVR实战之PMCore》(Yanlz+Unity+XR+SteamVR+VR+AR+MR+Valve+Oculus+立钻哥哥+==)