首页 > 代码库 > Servlet工作原理解析

Servlet工作原理解析

Web 技术成为当今主流的互联网 Web 应用技术之一,而 Servlet 是 Java Web 技术的核心基础。要介绍 Servlet 必须要先把 Servlet 容器说清楚,Servlet 与 Servlet 容器的关系有点像枪和子弹的关系,枪是为子弹而生,而子弹又让枪有了杀伤力。虽然它们是彼此依存的,但是又相互独立发展,这一切都是为了适应工业化生产的结果。从技术角度来说是为了解耦,通过标准化接口来相互协作。

Servlet 容器作为一个独立发展的标准化产品,目前它的种类很多,但是它们都有自己的市场定位,很难说谁优谁劣,各有特点。例如现在比较流行的 Jetty,在定制化和移动领域有不错的发展,这里还是以大家最为熟悉 Tomcat 为例来介绍 Servlet 容器如何管理 Servlet。

Servlet 容器Tomcat

Tomcat 的容器等级中,Context 容器是直接管理 Servlet 在容器中的包装类 Wrapper,所以 Context 容器如何运行将直接影响 Servlet 的工作方式。

技术分享

Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程。

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);

创建一个 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/catalin/startup/NO_DEFAULT_XML");         if (host == null) {             getHost().addChild(ctx);         } else {             host.addChild(ctx);         }         return ctx;  }

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

Tomcat 的启动逻辑是基于观察者模式设计的,所有的容器都会继承 Lifecycle 接口,它管理者容器的整个生命周期,所有容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener)。

当 Context 容器初始化状态设为 init 时,添加在 Context 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用上面的代码 时被加入到 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. 获取 ServletContext 并设置必要的参数
  8. 初始化“load on startup”的 Servlet

Web 应用的初始化工作

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

Tomcat 如何找到web.xml文件

  1. 首先会找 globalWebXml ,这个文件的搜索路径是在 engine 的工作目录下寻找以下两个文件中的任一个 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。
  2. 接着会找 hostWebXml ,这个文件可能会在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,
  3. 接着寻找应用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各个配置项将会被解析成相应的属性保存在 WebXml 对象中。
  4. 如果当前应用支持 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 指定。

Servlet 体系结构

技术分享

从上图可以看出 Servlet 规范就是基于这几个类运转的,与 Servlet 主动关联的是三个类,分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的,其中 ServletConfig(StandardWrapperFacade) 是在 Servlet 初始化时就传给 Servlet 了,而后两个是在请求达到时调用 Servlet 时传递过来的。查看 ServletConfig 接口中声明的方法发现,这些方法都是为了获取这个 Servlet 的一些配置属性,而这些配置属性可能在 Servlet 运行时被用到。而 ServletContext(ApplicationContextFacade) 又是干什么的呢? Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随个这个交易过程直到这个交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些指定参数通常就会是一个配置类。所以对号入座,交易场景就由 ServletContext 来描述,而定制的参数集合就由 ServletConfig 来描述,而 ServletRequest 和 ServletResponse 就是要交互的具体对象了。

ServletContext和ServletConfig 到底是个什么对象呢?

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

ApplicationContext和ApplicationContextFacade都实现了ServletContext接口,传给 Servlet 的是 ApplicationContextFacade 对象。这个类同样保证 ServletContext 只能从容器中拿到它该拿的数据,它们都起到对数据的封装作用,它们使用的都是门面设计模式。通过 ServletContext 可以拿到 Context 容器中一些必要信息,比如应用的工作路径,容器支持的 Servlet 最小版本等。

ServletRequest 和 ServletResponse 到底是个什么对象呢?

我们在创建自己的 Servlet 类时通常使用的都是 HttpServletRequest 和 HttpServletResponse,它们继承了 ServletRequest 和 ServletResponse。

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 和 RequestFacade,这里使用门面模式与前面一样都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示:

技术分享

创建 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

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

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

Servlet 如何工作(如何被调用)

当用户从浏览器向服务器发起一个请求,通常会包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用来与服务器建立 TCP 连接,而后面的 URL 才是用来选择服务器中那个子容器服务用户的请求

Tomcat7.0 中这种映射工作有专门一个类来完成的,这个就是 org.apache.tomcat.util.http.mapper,这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息,当 org.apache.catalina.connector. Request(由org.apache.coyote.Request而来) 类在进入 Container 容器之前,mapper 将会根据这次请求的 hostnane 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。所以当 Request 进入 Container 容器之前,它要访问那个子容器这时就已经确定了。

mapper 中怎么会有容器的完整关系,在MapperListener 类的初始化过程,下面是 MapperListener 的 init 方法代码 : 

public void init() {         findDefaultHost();         Engine engine = (Engine) connector.getService().getContainer();         engine.addContainerListener(this);         Container[] conHosts = engine.findChildren();         for (Container conHost : conHosts) {             Host host = (Host) conHost;             if (!LifecycleState.NEW.equals(host.getState())) {                 host.addLifecycleListener(this);                 registerHost(host);             }         }  }

这段代码的作用就是将 MapperListener 类作为一个监听者加到整个 Container 容器中的每个子容器中,这样只要任何一个容器发生变化,MapperListener 都将会被通知相应的保存容器关系的 MapperListener 的 mapper 属性也会修改。for 循环中就是将 host 及下面的子容器注册到 mapper 中。

请求到达最终的 Servlet 之前还要完成一些步骤,必须要执行 Filter 链,以及要通知你在 web.xml 中定义的 listener。假设已经完成了这些步骤,接下去就要执行 Servlet 的 service 方法了,通常情况下,我们自己定义的 servlet 并不是直接去实现 javax.servlet.servlet 接口,而是去继承更简单的 HttpServlet 类或者 GenericServlet 类,我们可以有选择的覆盖相应方法去实现我们要完成的工作(去实现 service 方法)。

当 Servlet 从 Servlet 容器中移除时,也就表明该 Servlet 的生命周期结束了,这时 Servlet 的 destroy 方法将被调用,做一些扫尾工作。

Servlet 中的 Listener

目前 Servlet 中提供了 6 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:

它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。

技术分享

如Spring的org.springframework.web.context.ContextLoaderListener就实现了一个ServletContextListener,当容器加载时启动spring容器。ContextLoaderListener在contextInitialized方法中初始化Spring容器(加载applicationContext.xml)。

Servlet 中的 Filter

Filter可以完成与Servlet同样的工作,甚至比其更加灵活,因为它除了提供request和response对象外,还提供了一个FilterChain对象,这个对象可以让我们更加灵活地控制请求的流转。

在Tomcat中,FilterConfig和FilterChain的实现类分别是ApplicationFilterConfig和ApplicationFilterChain,而Filter的实现类有用户自定义,只要实现Filter接口中定义的三个接口就行,这三个接口在与Servlet中的类似,只不过还有一个ApplicationFilterChain类,它可以将多个Filter串联起来,组成一个链,下面是Filter类中的三个接口方法:

      1.init(FilterConfig):初始化接口,在用户自定义的Filter初始化时被调用,它与Servlet的init方法的作用是一样的,FilterConfig与ServletConfig也类似,除了都能取到容器的环境类ServletContext对象外,还能获取在<filter>下配置的<init-param>参数值

      2.doFilter(ServletRequest, ServletResponse, FilterChain):在每个用户请求进来时这个方法都会被调用,并在Servlet的service方法被调用。而FilterChain就代表当前的整个请求链,所以可以通过FilterChain.doFilter()将请求继续传递下去。如果想拦截这个请求,就不调用FilterChain.doFilter(),那么这个请求就不返回了。所以Filter是一种责任链模式

      3.destroy:当Filter对象被销毁时,这个方法被调用。注意,当Web容器调用这个方法之后,容器会调用一次doFilter方法。 

Filter类的核心还是传递的FilterChain对象,这个对象保存了到最终Servlet对象的所有Filter对象,这些对象都保存在ApplicationFilterChain对象的filters数组中。在FilterChain链上每执行一个Filter对象,数组的当前计数都会加1,直到计数等于数组的长度,当FilterChain上所有的Filter对象执行完成,就会执行最终的Servlet。所以在ApplicationFilterChain对象中会持有Servlet对象的引用

Servlet 中的 url-pattern

在web.xml中<servlet-mapping>和<filter-mapping>都有配置项,它们的作用都是匹配一次请求是否会执行这个Servlet或者Filter。

Servlet的匹配是通过org.apache.tomcat.util.http.Mapper类完成的,这个类会根据请求到的URL来匹配在每个Servlet中配置的,所以它在一个请求被创建时就已经匹配了servlet是通过org.apache.tomcat.util.http.mapper.Mapper.internalMapWrapper方法中匹配的,servlet的匹配规则是精确匹配,路径匹配,后缀匹配,按顺序匹配,一次请求只能成功匹配到一个servlet。

Filter的url-pattern匹配是在创建ApplicationFilterChain对象时进行的,它会把所有定义的Filter的url-pattern与当前的URL匹配,如果匹配成功就将这个Filter保存到ApplicationFilterChain的filters数组中,然后在FilterChain中依次调用。filter是通过ApplicationFilterFactory.matchFiltersURL方法中匹配的。filter的匹配规则是,只要匹配成功,所有符合规则的filter都会被执行;

容器启动时会检查url-pattern是否符合规则,在web.xml加载时,会首先检查配置是否符合规则,这个检查是在StandardContext的validateURLPattern方法中检查的,如果检查不成功,Context容器启动会失败,并且会报java.lang.IllegalArgumentException:Invalid/a/*.htm in Servlet mapping错误。
<url-pattern>的解析工作,对servlet和filter是一样的,匹配规则有如下3种:
        (1)精确匹配:如/foo.htm只会匹配foo.htm这个URL。
        (2)路径匹配:如/foo/*会匹配以foo为前缀的URL。
        (3)后缀匹配:如*.htm会匹配所有以.htm为后缀的URL。

 

Servlet工作原理解析