Tomcat 管道与阀
以下部分内容摘抄自《深入剖析Tomcat》,该书籍研究对象为tomcat4和tomcat5,下文中有些知识已经过时。
本章节是"深析Tomcat容器工作流程"的续文。如有不理解的地方,请先阅读上文。
该章节旨在说明当连接器调用了servlet容器的invoke()方法后会发生什么事。然后,在对应小节中讨论org.apache.catalina包中的4个相关接口,Pipleline、Valve、ValveContext和Conntained。
管道包含该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;
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接口:
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。
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方法,管道和阀开始起作用。
Tomcat4中阀是Valve接口的实例,用来处理接收到的请求。该接口有两个方法,invoke()方法和getInfo()方法。invoke()方法已经在前面讨论过了,getInfo()方法返回阀的实现信息。代码清单5-4给出了Valve接口的定义。
在Tomcat7中上面管道章节中,我们也跟踪源码了,为什么tomcat会有管道和阀,而不是直接把请求交给StandardEngine,我想这也不必我多说,你心里多少也有些想法,有了管道和阀可以不断的对该管道中的阀进行调用,相当于一条链上的多个Filter一样,通过管道和阀去控制、设置请求和响应。
每个标准实现类都有一个标准实现类的阀,比如StandardEngine有StandardEngineValve,StandardHost有StandardHostValve。每个阀里的invoke各有特色,再次不在多说。
Tomcat4中阀可以选择是否实现org.apache.catalina.Contained接口,该接口的实现类可以通过接口中的方法至多与一个servlet容器相关联。代码清单5-6给出了Contained接口的定义。