Servlet 工做原理解析

-----转自许令波老师Servlet 工做原理解析  感受写的很不错,保存下来,留着之后温习java

从 Servlet 容器提及

      要介绍 Servlet 必需要先把 Servlet 容器说清楚,Servlet 与 Servlet 容器的关系有点像枪和子弹的关系,枪是为子弹而生,而子弹又让枪有了杀伤力。虽然它们是彼此依存的,可是又相互独立发展,这一切都是为了适应工业化生产的结果。从技术角度来讲是为了解耦,经过标准化接口来相互协做。既然接口是链接 Servlet 与 Servlet 容器的关键,那咱们就从它们的接口提及。web

前面说了 Servlet 容器做为一个独立发展的标准化产品,目前它的种类不少,可是它们都有本身的市场定位,很难说谁优谁劣,各有特色。例如如今比较流行的 Jetty,在定制化和移动领域有不错的发展,咱们这里仍是以你们最为熟悉 Tomcat 为例来介绍 Servlet 容器如何管理 Servlet。Tomcat 自己也很复杂,咱们只从 Servlet 与 Servlet 容器的接口部分开始介绍,关于 Tomcat 的详细介绍能够参考个人另一篇文章《 Tomcat 系统架构与模式设计分析》。apache

Tomcat 的容器等级中,Context 容器是直接管理 Servlet 在容器中的包装类 Wrapper,因此 Context 容器如何运行将直接影响 Servlet 的工做方式。设计模式

图 1 . Tomcat 容器模型

从上图能够看出 Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程,在 Tomcat 的配置文件中能够很容易发现这一点,以下tomcat

清单 1 Context 配置参数
 <Context path="/projectOne " docBase="D:\projects\projectOne" 
 reloadable="true" />
下面详细介绍一下 Tomcat 解析 Context 容器的过程,包括如何构建 Servlet 的过程。

Servlet 容器的启动过程

Tomcat7 也开始支持嵌入式功能,增长了一个启动类 org.apache.catalina.startup.Tomcat。建立一个实例对象并调用 start 方法就能够很容易启动 Tomcat,咱们还能够经过这个对象来增长和修改 Tomcat 的配置参数,如能够动态增长 Context、Servlet 等。下面咱们就利用这个 Tomcat 类来管理新增的一个 Context 容器,咱们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的。架构

清单 2 . 给 Tomcat 增长一个 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>") > 0);
清单 1 的代码是建立一个 Tomcat 实例并新增一个 Web 应用,而后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet,看有没有正确返回预期的数据。

Tomcat 的 addWebapp 方法的代码以下:app

清单 3 .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/catalin/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 中的访问路径和这个应用实际的物理路径,这个两个参数与清单 1 中的两个参数是一致的。其中最重要的一个配置是 ContextConfig,这个类将会负责整个 Web 应用配置的解析工做,后面将会详细介绍。最后将这个 Context 容器加到父容器 Host 中。
接下去将会调用 Tomcat 的 start 方法启动 Tomcat,若是你清楚 Tomcat 的系统架构,你会容易理解 Tomcat 的启动逻辑,Tomcat 的启动逻辑是基于观察者模式设计的,全部的容器都会继承 Lifecycle 接口,它管理者容器的整个生命周期,全部容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener),关于这个设计模式能够参考《 Tomcat 的系统架构与设计模式,第二部分:设计模式》。Tomcat 启动的时序图能够用图 2 表示。

图 2. Tomcat 主要类的启动时序图

上图描述了 Tomcat 启动过程当中,主要类之间的时序关系,下面咱们将会重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。webapp

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

ContextConfig 的 init 方法将会主要完成如下工做:ui

  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. 获取 ServletContext 并设置必要的参数
  8. 初始化“load on startup”的 Servlet

Web 应用的初始化工做

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

Tomcat 首先会找 globalWebXml 这个文件的搜索路径是在 engine 的工做目录下寻找如下两个文件中的任一个 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接着会找 hostWebXml 这个文件可能会在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,接着寻找应用的配置文件 examples/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 的代码片断:

清单 4. 建立 Wrapper 实例
 for (ServletDef servlet : servlets.values()) { 
            Wrapper wrapper = context.createWrapper(); 
            String jspFile = servlet.getJspFile(); 
            if (jspFile != null) { 
                wrapper.setJspFile(jspFile); 
            } 
            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.getAsyncSupported() != null) { 
                wrapper.setAsyncSupported( 
                        servlet.getAsyncSupported().booleanValue()); 
            } 
            context.addChild(wrapper); 
 }

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

除了将 Servlet 包装成 StandardWrapper 并做为子容器添加到 Context 中,其它的全部 web.xml 属性都被解析到 Context 中,因此说 Context 容器才是真正运行 Servlet 的 Servlet 容器。一个 Web 应用对应一个 Context 容器,容器的配置属性由应用的 web.xml 指定,这样咱们就能理解 web.xml 到底起到什么做用了。

建立 Servlet 实例

前面已经完成了 Servlet 的解析工做,而且被包装成 StandardWrapper 添加在 Context 容器中,可是它仍然不能为咱们工做,它尚未被实例化。下面咱们将介绍 Servlet 对象是如何建立的,以及如何被初始化的。

建立 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 对象的相关类结构图以下:

图 3. 建立 Servlet 对象的相关类结构
 
 

初始化 Servlet

初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,这个方法很简单就是调用 Servlet 的 init 的方法,同时把包装了 StandardWrapper 对象的 StandardWrapperFacade 做为 ServletConfig 传给 Servlet。Tomcat 容器为什么要传 StandardWrapperFacade 给 Servlet 对象将在后面作详细解析。

若是该 Servlet 关联的是一个 jsp 文件,那么前面初始化的就是 JspServlet,接下去会模拟一次简单请求,请求调用这个 jsp 文件,以便编译这个 jsp 文件为 class,并初始化这个 class。

这样 Servlet 对象就初始化完成了,事实上 Servlet 从被 web.xml 中解析到完成初始化,这个过程很是复杂,中间有不少过程,包括各类容器状态的转化引发的监听事件的触发、各类访问权限的控制和一些不可预料的错误发生的判断行为等等。咱们这里只抓了一些关键环节进行阐述,试图让你们有个整体脉络。

下面是这个过程的一个完整的时序图,其中也省略了一些细节。

图 4. 初始化 Servlet 的时序图
具体文章见:http://www.ibm.com/developerworks/cn/java/j-lo-servlet/
相关文章
相关标签/搜索