-----转自许令波老师Servlet 工做原理解析 感受写的很不错,保存下来,留着之后温习java
要介绍 Servlet 必需要先把 Servlet 容器说清楚,Servlet 与 Servlet 容器的关系有点像枪和子弹的关系,枪是为子弹而生,而子弹又让枪有了杀伤力。虽然它们是彼此依存的,可是又相互独立发展,这一切都是为了适应工业化生产的结果。从技术角度来讲是为了解耦,经过标准化接口来相互协做。既然接口是链接 Servlet 与 Servlet 容器的关键,那咱们就从它们的接口提及。web
前面说了 Servlet 容器做为一个独立发展的标准化产品,目前它的种类不少,可是它们都有本身的市场定位,很难说谁优谁劣,各有特色。例如如今比较流行的 Jetty,在定制化和移动领域有不错的发展,咱们这里仍是以你们最为熟悉 Tomcat 为例来介绍 Servlet 容器如何管理 Servlet。Tomcat 自己也很复杂,咱们只从 Servlet 与 Servlet 容器的接口部分开始介绍,关于 Tomcat 的详细介绍能够参考个人另一篇文章《 Tomcat 系统架构与模式设计分析》。apache
Tomcat 的容器等级中,Context 容器是直接管理 Servlet 在容器中的包装类 Wrapper,因此 Context 容器如何运行将直接影响 Servlet 的工做方式。设计模式
从上图能够看出 Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程,在 Tomcat 的配置文件中能够很容易发现这一点,以下tomcat
<Context path="/projectOne " docBase="D:\projects\projectOne" reloadable="true" />
下面详细介绍一下 Tomcat 解析 Context 容器的过程,包括如何构建 Servlet 的过程。
Tomcat7 也开始支持嵌入式功能,增长了一个启动类 org.apache.catalina.startup.Tomcat。建立一个实例对象并调用 start 方法就能够很容易启动 Tomcat,咱们还能够经过这个对象来增长和修改 Tomcat 的配置参数,如能够动态增长 Context、Servlet 等。下面咱们就利用这个 Tomcat 类来管理新增的一个 Context 容器,咱们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的。架构
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
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 表示。
上图描述了 Tomcat 启动过程当中,主要类之间的时序关系,下面咱们将会重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。webapp
当 Context 容器初始化状态设为 init 时,添加在 Contex 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用清单 3 时被加入到 StandardContext 容器中。ContextConfig 类会负责整个 Web 应用的配置文件的解析工做。jsp
ContextConfig 的 init 方法将会主要完成如下工做:ui
ContextConfig 的 init 方法完成后,Context 容器的会执行 startInternal 方法,这个方法启动逻辑比较复杂,主要包括以下几个部分:
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 的代码片断:
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 的解析工做,而且被包装成 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 在 StandardWrapper 的 initServlet 方法中,这个方法很简单就是调用 Servlet 的 init 的方法,同时把包装了 StandardWrapper 对象的 StandardWrapperFacade 做为 ServletConfig 传给 Servlet。Tomcat 容器为什么要传 StandardWrapperFacade 给 Servlet 对象将在后面作详细解析。
若是该 Servlet 关联的是一个 jsp 文件,那么前面初始化的就是 JspServlet,接下去会模拟一次简单请求,请求调用这个 jsp 文件,以便编译这个 jsp 文件为 class,并初始化这个 class。
这样 Servlet 对象就初始化完成了,事实上 Servlet 从被 web.xml 中解析到完成初始化,这个过程很是复杂,中间有不少过程,包括各类容器状态的转化引发的监听事件的触发、各类访问权限的控制和一些不可预料的错误发生的判断行为等等。咱们这里只抓了一些关键环节进行阐述,试图让你们有个整体脉络。
下面是这个过程的一个完整的时序图,其中也省略了一些细节。
具体文章见:http://www.ibm.com/developerworks/cn/java/j-lo-servlet/