Servlet工作原理解析1(以Tomcat为例)


Servlet与Servlet容器就像枪和子弹的关系,相互依存又独立发展。Servlet容器作为一个独立发展的标准化产品,目前种类很多,但它们都有自己的市场定位,各有特点,很难说谁优谁劣。例如,Jetty现在比较流行,在定制化和移动领域有不错的发展。下面将以Tomcat为例分析Servlet容器是如何管理Servlet的。
在Tomcat的容器等级中,Context容器直接管理Servlet在容器中的包装类Wrapper,所以Context容器如何运行将直接影响Servlet的工作方式。

上图是Tomcat容器模型。真正管理Servlet的容器是Context容器,一个Context对应一个Web工程,在Tomcat的配置文件中可以很容易地发现这一点:
<Context path="/projectOne" docBase="D:\projects\projectOne" reloadable="true" />


Servlet容器的启动过程:

Tomcat7也开始支持嵌入式功能,增加了一个启动类org.apache.catalina.startup.Tomcat。创建一个实例对象并调用start方法就可以很容易地启动Tomcat,还可以通过这个对象来增加和修改Tomcat的配置参数,如可以动态增加Context、Servlet等。下面就利用这个Tomcat类来管理一个新增的Context容器,选择Tomcat7自带的examples Web工程。

Tomcat tomcat = getTomcatInstance();

File appDir = new File(getBuildDirectory(), "webapps/examples");

tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath());

tomcat.start();

ByteChunk res = getUrl("http://localhost:"+ getPort() + "/examples/servlets/servlet/HelloWorldExample");

assertTrue(res.toString().indexOf("<h1>Hello World!</h1>"));

这段代码创建了一个Tomcat实例并新增了一个Web应用,然后启动Tomcat并调用其中的一个HelloWorldExample Servlet,看看有没有正确返回预期的数据。

Tomcat的addWebapp方法的代码如下:

public Context addWebapp(Host host, String url, String path){

silence(url);

Context ctx = new StandardContext();

ctx.setPath(url);

ctx.setDocBase(path);

if(defaultRealm == null){

initSimpleAuth();

}

ctx.setRealm(defaultRealm);

ctx.addLifecycleListener(new DefaultWebXmlListener());

ContextConfig ctxCfg = new ContextConfig();

ctx.addLifecycleListener(ctxCfg);

ctxCfg.setDefaultWebXml("org/apache/catalina/startup/NO_DEFAULT_XML");

if(host == null){

getHost().addChild(ctx);

}else{

host.addChild(ctx);

}

return ctx;

}

一个Web应用对应一个Context容器,也就是Servlet运行时的Servlet容器。添加一个Web应用时将会创建一个StandardContext容器,并且给这个Context容器设置必要的参数,url和path分别代表这个应用在Tomcat中的访问路径和这个应用实际的物理路径,这两个参数跟Tomcat配置中的两个参数是一致的。其中最重要的一个配置是ContextConfig,这个类将会负责整个Web应用配置的解析工作。最后将这个Context容器加到父容器Host中。

接下来将会调用Tomcat的start方法启动Tomcat。它的启动逻辑是基于观察者模式设计的,所有的容器都会继承Lifecycle接口,它管理着容器的整个生命周期,所有的容器的修改和状态的改变都会由它去通知已经注册的观察者(Listener)。下面是Tomcat启动的时序图:

上图是Tomcat的启动过程中主要类之间的时序关系。

下面是添加examples应用时所对应的StandardContext容器的启动过程。

当Context容器初始化状态为init时,添加到Context容器的Listener将会被调用。ContextConfig继承了LifecycleListener接口,它是在调用Tomcat.addWebapp时被加入到StandardContext容器中的。ContextConfig类会负责整个Web应用的配置文件的解析工作。

ContextConfig的init方法将会主要完成以下工作:

1.创建用于解析XML配置文件的contextDigester对象;

2.读取默认的context.xml配置文件,如果存在则解析它;

3.读取默认的Host配置文件,如果存在则解析它;

4.读取默认的Context自身的配置文件,如果存在则解析它;

5.设置Context的DocBase。

ContextConfig的init方法完成后,Context容器就会执行startInternal方法,这个方法的启动逻辑比较复杂,主要包括如下几部分:

1.创建读取资源文件的对象

2.创建ClassLoader对象

3.设置应用的工作目录

4.启动相关的辅助类,如logger、realm、resources等

5.修改启动状态,通知感兴趣的观察者(Web应用的配置)

6.子容器的初始化

7.获取ServletConfig并设置必要的参数

8.初始化"load on startup"的Servlet

Web应用的初始化工作:

Web应用的初始化工作是在ContextConfig的configureStart方法中实现的,应用的初始化主要是解析web.xml文件,这个文件描述了一个Web应用的关键信息,也是一个Web应用的入口。

Tomcat首先会找globalWebXml,这个文件的搜索路径是engine的工作目录下的org/apache/catalina/startup/NO_DEFAUL_XML或conf/web.xml。接着会找hostWebXml,这个文件可能会在System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default中,接着寻址应用的配置文件example/WEB-INF/web.xml。web.xml文件中的各个配置项将会被解析成相应的属性保存在WebXml对象中。如果当前的应用支持Servlet3.0,解析还将完成额外的9项工作,这9项额外的工作主要是Servlet3.0新增的特性(包括jar包中的META-INF/web-fragment.xml)的解析及对annotations的支持。

接下来会将WebXml对象中的属性设置到Context容器中,这里包括创建Servlet对象、filter、listener等,这段代码在WebXml的configureContext方法中。下面是解析Servlet的代码片段:

for(ServletDef servlet : servlets.values()){

Wrapper wrapper = context.createWrapper();

String jspFile = servlet.getJspFile();

if( jspFile != null){

wrapper.setJspFile();

}

if(servlet.getLoadOnStartup() != null){

wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());

}

if(servlet.getEnabled() != null){

wrapper.setEnabled(servlet.getEnabled().booleanValue());

}

wrapper.setName(servlet.getServletName());

Map<String, String> params = servlet.getParameterMap();

for(Entry<String, String) entry : params.entrySet()){

wrapper.addInitParameter(entry.getKey(), entry.getValue());

}

wrapper.setRunAs(servlet.getRunAs());

Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();

for(SecurityRoleRef roleRef : roleRefs){

wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink());

}

wrapper.setServletClass(servlet.getServletClass);

MultipartDef multipartdef = servlet.getMultipartDef();

if(multipartdef != null){

if(multipartdef.getMaxFileSize() != null && multipartdef.getMaxRequestSize != null && multipartdef.getFileSizeThreshold() !=null){

wrapper.setMultipartConfigElement(

new MultipartConfigElement(

multipartdef.getLocation(),

Long.parseLong(multipartdef.getMaxFileSize()),

Long.parseLong(multipartdef.getMaxRequestSize()),

Integer.parseInt(multipartdef.getFileSizeThreshold())));

}else{

wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation()));

}

}

if(servlet.getAsynSupported() != null){

wrapper.setAsyncSupported(servlet.getAsynSupported().booleanValue());

}

context.addChild(wrapper);

}

这段代码清楚地描述了如何将Servlet包装成Context容器中的StandardWrapper,这里有个疑问,为什么要将Servlet包装成StandardWrapper而不是直接包装成Servlet对象?因为StandardWrapper是Tomcat容器中的一部分,它具有容器的特征,而Servlet作为一个独立的web开发标准,不应该强耦合在Tomcat中。

除了将Servlet包装成StandardWrapper冰作为子容器添加到Context中外,其它所有的web.xml属性都被解析到Context中,所以说Context容器才是真正运行Servlet的Servlet容器。

创建Servlet容器

前面已经完成了Servlet的解析工作,并且被包装成StandardWrapper添加在Context容器中,但是它仍不能为我们工作,它并没有被实例化。

创建Servlet对象:如果Servlet的load-on-startup配置项大于0,那么在Context容器启动时就会被实例化,前面提到在解析配置文件时会读取默认的globalWebXml,在conf下的web.xml文件中定义了一些默认的配置项,其中定义了两个Servlet,分别是org.apache.catalina.servlets.DefaultServlet和org.apache.jasper.servlet.JspServlet

。它们的load-on-startup分别是1和3,也就是当Tomcat启动时这两个Servlet就会被启动。

创建Servlet实例的方法是从Wrapper.loadServlet开始的。loadServlet方法要完成的就是获取servletClass,然后把它交给InstanceManager去创建一个基于servletClass.class的对象。如果这个Servlet配置了jsp-file,那么这个servletClass就是在conf/web.xml中定义的org.apache.jasper.servlet.JspServlet了。

下面是创建Servlet对象的相关类结构:

初始化Servlet:

初始化Servlet在StandardWrapper的initServlet方法中,这个方法很简单,就是调用Servlet的init()方法,同时把包装了StandardWrapper对象的StandardWrapperFacade作为ServletConfig传给Servlet。如果Servlet关联的是一个JSP文件,那么前面初始化的就是JspServlet。

事实上Servlet从被web.xml解析到完成初始化,这个过程非常复杂,中间有很多过程,包括各种容器状态的转化引起的监听事件的触发、各种访问权限的控制和一些不可预料的错误发生的判断行为等。下图是这个过程的一个时序图:

Servlet体系结构

Java Web应用是基于Servlet规范运转的,那么Servlet本身又是如何运转的呢?下图是Servlet顶层类关联图:

Servlet规范就是基于上面几个类运转的。与Servlet主动关联的是三个类,分别是ServletConfig、ServletRequest和ServletResponse。这三个类都是通过容器传递给Servlet的,其中ServletConfig在Servlet初始化时就传给它了,后两个是在请求到达时调用它从而传递过来的。ServletConfig和ServletContext对Servlet的作用:ServletContext描述Servlet的交易场景,而定制的参数集合就是由ServletConfig来描述。在ServletConfig接口中声明的方法都是为了获取这个Servlet的一些配置属性,而这些配置属性可能在Servlet运行时被用到。握手型的交互式是Servlet的运行模式。所谓握手型的交互式就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随这个交易过程直到这个交易完成为止。这个交易场景的初始化时根据这次交易对象指定的参数来制定的,这些指定参数通常就是一个配置类。而ServletRequest和ServletResponse就是要交互的具体对象,它们通常都作为运输工具来传递交互结果。

下图是ServletConfig和ServletContext在Tomcat容器中的类关系图:

可以看出StandardWrapper和StandardWrapperFacade都实现了ServletConfig接口,而StandardWrapperFacade是StandardWrapper门面类。所以传给Servlet的是StandardWrapperFacade对象,这个类能够保证从StandardWrapper中拿到ServletConfig所规定的数据,而又不把ServletConfig不关心的数据暴露个Servlet。

同样ServletContext也与ServletConfig有类似的结构,在Servlet中能拿到的ServletContext的实际对象也是ApplicationContextFacade对象。ApplicationContextFacade同样保证ServletContext只能从容器中拿到它该拿的数据,它们都起到对数据的封装作用,它们使用的都是门面设计模式。通过ServletContext可以拿到Context容器中的一些必要信息,如应用的工作路径、容器支持的Servlet最小版本等。

此图是Tomcat创建的Request与Response的类结构图:

Tomcat接到请求首先将会创建org.apache.coyote.Request和org.apache.coyote.Response,这两个类是Tomcat内部使用的描述一次请求和相应的信息类,它们是一个个轻量级的类,用于在服务器接收到请求后,经过简单解析将这个请求快速分配给后续线程处理,所以它们的对象很小,很容易被JVM回收。接下来当交给一个用户线程去处理这个请求时有创建org.apache.catalina.connector.Request和org.apache.catalina.connector.Response对象。这两个对象一直贯穿整个Servlet容器直到要传给Servlet,传给Servlet的是Request和Response的门面类RequestFacade和ResponseFacade。

下图是Request和Response的转变过程:


优质内容筛选与推荐>>
1、mysql操作手册
2、DOM
3、[1024-1025]模拟赛
4、随心随笔
5、img 鼠标 在滚动条 滚动到 顶端附近时 图片的上部 鼠标形状不起作用. 再往上滚. 择全部图片的鼠标形状不起作用.


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号