您的位置:首页 > 运维架构 > Tomcat

深入剖析Tomcat 第五章 Servlet容器

2017-03-27 11:23 387 查看

第一部分:概述

这一节基于《深度剖析Tomcat》第五章:servlet容器总结而成。

Servlet容器是用来处理请求Servlet资源,并为web客户端填充response对象的模块。要想连接器能调用

这一部分会先给先总结一下Tomcat连接器的相关内容,然后是Tomcat容器的总体介绍,最后在下一部分进入本章的主题: Tomcat 容器的详细讲解。

Tomcat 连接器总结

我们都知道Catalina的核心组件包括:连接器和容器,那连接器是干嘛的呢?前面讲过,总的来说就是等待用户的请求,接收用户请求,解析请求,封装成请求对象和响应对象,然后将请求和响应对象交给容器处理。

整个过程的细节包括了:创建ServerSocket对象创建处理器池(在每个处理器内创建请求和响应对象,然后启动所有处理器线程,但会阻塞,因为没有请求到达,无法处理),然后等待用户请求(阻塞), 用户请求到来时,获取其中一个处理器线程,将socket交给处理器处理,以唤醒处理器线程(利用available变量),让处理器线程解析请求,具体方法是解析端口,解析请求头等信息,解析完后交给容器真正处理请求的内容(如servlet,就是调用我们常说的invoke方法),处理完后就讲处理器的所有变量恢复到未被使用前,最后回收回线程池。(调用处理器线程的process(socket)方法,进入容器的service()方法)

connector.getContainer().invoke(request, response);


Tomcat 容器概述

servlet容器的invoke()方法,需要将servlet容器的实例作为参数传入到连接器的setContainer()方法中。

前面几节我们都在介绍Tomcat连接器是如何实现的,也学到了它实现的巧妙之处,而这一节要开始学习Tomcat的另一个组件:容器。和连接器一样,容器有着不可忽略的作用,我们看到连接器从头到尾都是在做一些基础的工作,从等待请求到解析请求,却没有参与到处理用户真正想要的东西(如servlet要进行某些操作),所以说容器是连接器基础工作做完后,做真正的请求处理操作(加载servlet)。看到上面的连接器总结没,invoke方法就是本节我们要讲解的重中之重—-获取容器执行invoke方法

容器是一个处理用户servlet请求并返回对象给web用户的模块。org.apache.catalina.Container 接口定义了容器的形式。有四种容器: Engine(引擎) , Host(主机) , Context(上下文) , 和 Wrapper(包装器)。我也按照书上的逻辑,先介绍Context和Wrapper,其它两个留以后讲解。

对于 Catalina 的容器首先需要注意的是它一共有四种不同的容器:

Engine:表示整个Catalina的servlet引擎;

Host:表示一个拥有数个上下文的虚拟主机;

Context:表示一个Web应用,一个context包含一个或多个wrapper;

Wrapper:表示一个独立的 servlet;

它们的标准实现是 StandardEngine,StandardHost, StandardContext, and StandardWrapper,它们都是org.apache.catalina.core 包的一部分,这些类都扩展了抽象类 ContainerBase。

要实现一个容器,首先必须要实现 org.apache.catalina.Container 接口。从上一节的连接器讲解中我们也看到了ex04.pyrmont.core.SimpleContainer也实现了Container接口,实现了简单的servlet的功能

public class SimpleContainer implements Container {
public void invoke(Request request, Response response) throws IOException,
ServletException {
//中间省略
}
}


同时,我们需要知道的是:并不是每一个功能都需要用到四种容器,下面的程序中会看到。

一个容器可以有一个或多个低层次上的子容器,例如,一个 Context 有一个或多个 wrapper,而 wrapper 作为容器层次中的最底层,不能包含子容器。可以使用在Container接口中定义的 addChild()方法,wrapper调用会抛出异常。

容器的整个处理流程通过一个叫做Pipeline(流水线)的接口实现,这个接口的功能是放置一些Valve(阀门),这些阀门的功能是执行一些基础功能操作,比如记录日志,记录ip等,像流水线一样,通过阀门上下文接口(ValveContext)调用下一个阀门,最后进入一个基础阀门,实现用户真正的请求。

Tomcat 容器核心接口:

Pipeline: 一个pipeline包含了该容器要唤醒的所有任务。每一个阀门表示了一个特定的任务。一个容器的流水线有一个基本的阀门,但是你可以添加任意你想要添加的阀门。阀门的数目定义为添加的阀门的个数(不包括基本阀门)。

Valve:阀门接口表示一个阀门,该组件负责处理请求。
ValveContext: 用于切换到下一个阀门


Contained:一个阀门可以选择性的实现 org.apache.catalina.Contained 接口。该接口定义了其实现类跟一个容器相关联。包含 getContainer和setContainer 方法

Wrapper:org.apache.catalina.Wrapper 接口表示了一个包装器。一个包装器是表示一个独立 servlet 定义的容器。包装器继承了 Container 接口,并且添加了几个方法。包装器的实现类负责管理其下层 servlet 的生命周期,包括 servlet 的 init,service,destroy 方法。

Context:一个 context 在容器中表示一个 web 应用。一个 context 通常含有一个或多个包装器作为其子容器。

第二部分:详细讲解

为了模拟容器的实现,容器,流水线等都是用ex05.pyrmont包下相关的类,如SimpleWrapper等,而不是去查看StandardWrapper源代码

启动类Bootstrap1.java

同样的,我们先接触启动类ex05.pyrmont.startup.Bootstrap1.java,这个类是对单个servlet的处理,所以可以看到只用了wrapper,没有用到context。如果我们要访问,可以用localhost:8080/ModernServlet

public final class Bootstrap1 {

public static void main(String[] args) {

HttpConnector connector = new HttpConnector();
//定义一个包装器类(容器),实际上connector.getContainer().invoke(request, response);   //就是调用SimpleWrapper对象的invoke方法
Wrapper wrapper = new SimpleWrapper();
wrapper.setServletClass("ModernServlet");

//定义一个类加载器
Loader loader = new SimpleLoader();
//下面就是两个阀门
Valve valve1 = new HeaderLoggerValve();
Valve valve2 = new ClientIPLoggerValve();
//容器绑定类加载器
wrapper.setLoader(loader);
//容器添加阀门
((Pipeline) wrapper).addValve(valve1);
((Pipeline) wrapper).addValve(valve2);

//连接器绑定容器,就是connector.getContainer().invoke时要将请求交给容器处理
connector.setContainer(wrapper);

try {
//下面两个方法是连接器相关内容,不再重复
connector.initialize();
connector.start();

// make the application wait until we press a key.
System.in.read();
}
catch (Exception e) {
e.printStackTrace();
}
}
}


当然,直接看注释是没用的,因为我们不清楚我们为什么要这样做。不急,下面慢慢讲解。记得不断调回这个类整理逻辑哦

SimpleWrapper.java

这个类算是本节比较重要的类了,大部分的接口都可以在这里看到。 SimpleWrapper是invoke出现的地方。

下面贴上比较重要的代码和相应注释

public class SimpleWrapper implements Wrapper, Pipeline {

// the servlet instance
private Servlet instance = null;       //加载到的servlet实例
private String servletClass;
private Loader loader;                 //类加载器
private String name;
//从一开始就绑定创建的流水线是该容器
private SimplePipeline pipeline = new SimplePipeline(this);
protected Container parent = null;

public SimpleWrapper() {
//其实我们在创建SimpleWrapper对象时就设置流水线的基础阀门,通过setBasic
pipeline.setBasic(new SimpleWrapperValve());
}

public synchronized void addValve(Valve valve) {
//为流水线添加其他Valve
pipeline.addValve(valve);
}

//这个方法的作用是加载Servlet,获取到Servlet实例
public Servlet allocate() throws ServletException {
// Load and initialize our instance if necessary
if (instance==null) {
try {
instance = loadServlet();
}
catch (ServletException e) {
throw e;
}
catch (Throwable e) {
throw new ServletException("Cannot allocate a servlet instance", e);
}
}
return instance;
}

//具体加载servlet的方法
private Servlet loadServlet() throws ServletException {
if (instance!=null)
return instance;

Servlet servlet = null;
String actualClass = servletClass;
if (actualClass == null) {
throw new ServletException("servlet class has not been specified");
}

Loader loader = getLoader();//获取到我们定义的类加载器类SimpleLoader,定义和我们需要的加载方式
// Acquire an instance of the class loader to be used
if (loader==null) {
throw new ServletException("No loader.");
}
ClassLoader classLoader = loader.getClassLoader();

// Load the specified servlet class from the appropriate class loader
Class classClass = null;
try {
if (classLoader!=null) {
classClass = classLoader.loadClass(actualClass);//加载servlet
}
}
catch (ClassNotFoundException e) {
throw new ServletException("Servlet class not found");
}
// Instantiate and initialize an instance of the servlet class itself
try {
servlet = (Servlet) classClass.newInstance();//创建servlet实例
}
catch (Throwable e) {
throw new ServletException("Failed to i
c3d7
nstantiate servlet");
}

// Call the initialization method of this servlet
try {
//这里调用了servlet的初始化方法,所有跟我们以前学过的知识一样,init方法只调用一次
servlet.init(null);
}
catch (Throwable f) {
throw new ServletException("Failed initialize servlet.");
}
return servlet;
}

public Loader getLoader(){
if(loader != null)
return (loader);
if(parent != null)
return (parent.getLoader);
return (null);
}

//就是调用这个invoke方法
public void invoke(Request request, Response response)
throws IOException, ServletException {
pipeline.invoke(request, response);
}


这个类里面有几个比较关心的方法:

invoke方法:我们讲解的入口

addValve方法:添加阀门,其中基础阀门在创建SimpleWrapper实例时就设置了

allocate方法:获取类加载器,获取servlet实例

invoke方面里面的是pipeline.invoke(request, response),前面我们说到,Pineline的作用是负责处理多个任务,

有的人就认为为什么不直接调用我们的Valve,甚至说不直接去掉Pineline,这里我要说一下,我们不能简简单单的认为容器就只是处理我们要做的任务,还有很多任务要做的,比如记录是什么IP操作过的,这些就像我们程序中用的拦截器一样,需要先经过很多的中间步骤。所以回到Bootstrap1.java中的addValve操作,和setLoader方法,可以理解了吧。当然,addValve要真正理解需要继续看下去

那流水线的invoke是如何实现多个任务按顺序处理执行的呢?

SimpleWrapper.java中一开始就创建流水线对象,并绑定容器为SimpleWrapper,因为invoke中要用到

pipeline.invoke(request, response);


下面查看部分关键代码

public class SimplePipeline implements Pipeline {

public void invoke(Request request, Response response)
throws IOException, ServletException {
// Invoke the first Valve in this pipeline for this request
(new SimplePipelineValveContext()).invokeNext(request, response);
}
//添加阀门
public void addValve(Valve valve) {
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);

synchronized (valves) {
Valve results[] = new Valve[valves.length +1];
System.arraycopy(valves, 0, results, 0, valves.length);
results[valves.length] = valve;
valves = results;
}
}
public void setBasic(Valve valve) {
this.basic = valve;
((Contained) valve).setContainer(container);
}
//SimplePipeline的内部类,实现ValveContext接口,负责切换到下一个阀门
protected class SimplePipelineValveContext implements ValveContext {

protected int stage = 0;

public String getInfo() {
return null;
}

public void invokeNext(Request request, Response response)
throws IOException, ServletException {
int subscript = stage;
stage = stage + 1;
// Invoke the requested Valve for the current request thread
if (subscript < valves.length) {//先调用设置的阀门
valves[subscript].invoke(request, response, this);
}
else if ((subscript == valves.length) && (basic != null)) {
basic.invoke(request, response, this);//最后才是basic阀门
}
else {
throw new ServletException("No valve");
}
}
} // end of inner class

}


需要注意的是:我专门加setBasic方法作为关键代码贴出来,是为了提醒大家SimplePineline上在SimpleWrapper中已经设置了基本Valve,

我们看到SimplePipeline的invoke方法里面是创建一个内部类SimplePipelineValveContext对象:这个对象负责流水线上阀门Valve的切换,这个内部类实现了ValveContext接口,ValveContext接口中其中一个方法是invokeNext方法,用来调取下一个Valve,具体的实现也不难

invokeNext 方法使用下标(subscript)和级别( stage)记住哪个阀门被唤醒。当第一次唤醒的时候,下标的值是 0,级的值是 1。第一次,第一个阀门被唤醒,流水线的阀门获得 ValveContext 实例调用它的 invokeNext 方法。这时下标的值是 1 所以下一个阀门被唤醒,然后一步步的进行。

下一小节,我们先选择其中一个阀门讲解

HeaderLoggerValve.java

这个类实现了Valve接口和Contained接口,我们知道Valve的接口功能是负责处理请求,所以接口里面有invoke方法

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


HeaderLoggerValve.java是用来输出请求头的内容。

public class HeaderLoggerValve implements Valve, Contained {

protected Container container;

public void invoke(Request request, Response response, ValveContext valveContext)
throws IOException, ServletException {

// Pass this request on to the next valve in our pipeline
valveContext.invokeNext(request, response);//执行下一个阀门

System.out.println("Header Logger Valve");
ServletRequest sreq = request.getRequest();
if (sreq instanceof HttpServletRequest) {
HttpServletRequest hreq = (HttpServletRequest) sreq;
Enumeration headerNames = hreq.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement().toString();
String headerValue = hreq.getHeader(headerName);
System.out.println(headerName + ":" + headerValue);
}
}
else{
System.out.println("Not an HTTP Request");
}
System.out.println("------------------------------------");
}
}


invoke(Request request, Response response, ValveContext context)方法中需要注意的是第三个参数ValveContext context, 我们会看到SimplePipeline里面传递了this引用,在某个阀门调用context的invokeNext方法时

valveContext.invokeNext(request, response);

以保证下标(subscript)和级别(stage)的值是正确的(因为是同一个引用),下一次执行的阀门就是valve[1], 也就是ClientIPLoggerValve。

最后调用的是基础阀门

basic.invoke(request, response, this);//最后才是basic阀门

SimpleWrapperValve.java

SimpleWrapperValve.java也是一个阀门,所以也实现了Valve和Contained接口

public class SimpleWrapperValve implements Valve, Contained {

protected Container container;

public void invoke(Request request, Response response, ValveContext valveContext)
throws IOException, ServletException {

SimpleWrapper wrapper = (SimpleWrapper) getContainer();
ServletRequest sreq = request.getRequest();
ServletResponse sres = response.getResponse();
Servlet servlet = null;
HttpServletRequest hreq = null;
//看到没,这里强制转换为HttpServletRequest和HttpServletResponse
if (sreq instanceof HttpServletRequest)
hreq = (HttpServletRequest) sreq;
HttpServletResponse hres = null;
if (sres instanceof HttpServletResponse)
hres = (HttpServletResponse) sres;

// Allocate a servlet instance to process this request
try {
//加载我们想访问的真正的servlet,这个方法上面讲了
servlet = wrapper.allocate();
if (hres!=null && hreq!=null) {
servlet.service(hreq, hres);//这里调用了service
}
else {
servlet.service(sreq, sres);
}
}
catch (ServletException e) {
}
}

public String getInfo() {
return null;
}

public Container getContainer() {
return container;
}

public void setContainer(Container container) {
this.container = container;
}
}


SimpleWrapperValve作为基础阀门,终于找到我们梦寐已久的service方法

其中SimpleWrapper中我们看到了这个容器是如何加载servlet的,加载的时候调用了servlet的init方法,

也就是基础阀门的invoke方法中调用加载初始化(wrapper.allocate())

接下来调用了servlet的service方法,

我们看到servlet是加载一次的而已,所以我们可以得到结论:Servlet不是线程安全的(这次不再是只看结论了哦,我们是推出来的,实在不信,可以看看Standard*的源代码)

第三部分:小结

Tomcat容器 理解代码还是次要的 最重要的是明白 Tomcat 为什么要这样进行模块设计,我们可以好好学习这种思想,为什么要分这些接口, 不管读什么,读源代码就是这样,在理解是如何本身是如何实现的基础上,理解设计思想。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  tomcat