servlet工作原理之tomcat篇

目录

servlet容器

servlet容器的启动过程

web应用的初始化

创建servlet实例

创建servlet对象

初始化servlet

servlet体系结构

ServletConfig

ServletRequest和ServletResponse

servlet如何工作

Session与cookie

servlet中的listener


本文概要见另一篇文章:https://blog.csdn.net/java_2017_csdn/article/details/78248127

servlet容器

servlet容器作为一个独立发展的标准化产品,种类很多。例如jetty,在定制化和移动领域有不错的发展,我这里以tomcat为例介绍servlet容器如何管理servlet。tomcat容器等级中,context容器是直接管理servlet在容器中的包装类wrapper,所以context容器如何运行将直接影响servlet的工作方式。

图1. tomcat容器模型

servlet工作原理之tomcat篇_第1张图片

 

servlet容器的启动过程

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

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

图2 tomcat主要类的启动时序图:servlet工作原理之tomcat篇_第2张图片

当context容器初始化状态为init时,添加在context容器的listener将会被调用。ContextConfig继承了LifecycleListener接口,它是在调用清单3时被加入到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首先会找globalWebXml这个文件的搜索路径,是在engine的工作目录下寻找以下两个文件中的任一个:org/apache/catalina/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 params = servlet.getParameterMap(); 
           for (Entry entry : params.entrySet()) { 
               wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
           } 
           wrapper.setRunAs(servlet.getRunAs()); 
           Set 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。这里StandardWrapper是tomcat容器中的一部分,它具有容器的特征;而servlet是一个独立的web开发标准,不应该强耦合在tomcat中。

除了将servlet包装成StandardWrapper并作为子容器添加到context中,其他的所有web.xml属性都被解析到context中,所以说context容器才是真正运行servlet的容器。一个web应用对应一个context容器,容器的配置属性由应用的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了。

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

servlet工作原理之tomcat篇_第3张图片

初始化servlet

初始化servlet在StandardWrapper的initServlet方法中,这个方法很简单,就是调用servlet的init方法,同时把包装了StandardWrapper对象的StandardWrapperFacade作为ServletConfig传给servlet。

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

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

图4. 初始化servlet的时序图

servlet工作原理之tomcat篇_第4张图片

servlet体系结构

我们知道Java web 应用是基于servlet规范运转的,那么sevlet本身又是如何运转的呢?为何要设计这样的体系结构。

图5. servlet 顶层类关联图

servlet工作原理之tomcat篇_第5张图片

从上图可以看出servlet规范就是基于这几个类运转的,与servlet主动关联的是三个类,分别是ServletConfig、ServletRequest和ServletResponse。这三个类都是通过容器传给servlet的,其中ServletConfig是在servlet初始化时就传给servlet了,而后两个是在请求达到时调用servlet时传递过来的。

ServletConfig

仔细看servletConfig接口中声明的方法,可以发现这些方法都是为了获取这个servlet的一些配置属性,而这些配置属性可能在servlet运行时被用到。而ServletContext又是干什么的呢?

servlet的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随这个交易过程直到交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些指定参数通常就会是一个配置类。对号入座,交易场景就由ServletContext来描述,而定制的参数集合就由ServletConfig来描述。ServletConfig是在Servlet.init时由容器传过来的。

图6. ServletConfig在容器中的类关联图

servlet工作原理之tomcat篇_第6张图片

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

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

ServletRequest和ServletResponse

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

图7. request相关类结构图

servlet工作原理之tomcat篇_第7张图片

上图是tomcat创建的request和response的类结构图。tomcat一接受到请求首先会创建org.apache.coyote.Request和org.apache.coyote.Response,这两个类是Tomcat内部使用的描述一次请求和相应信息的类,它们是一个轻量级的类,作用就是在服务器接收到请求后,经过简单解析将这个请求快速地分配给后续线程去处理。

接下去当交给一个用户线程去处理这个请求时又创建org.apache.catalina.connector.Request和org.apache.catalina.connector.Response对象。这两个对象一直穿越整个Servlet容器直到要传给Servlet,传给servlet的是request和response的门面类RequestFacade和ResponseFacade,这里使用门面模式与前面一样都是基于同样的目的——封装容器中的数据。

图8.一次请求中request和response的转变过程

servlet工作原理之tomcat篇_第8张图片

servlet如何工作

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

那服务器是如何根据这个url达到正确的servlet容器中的呢?这种映射工作有专门一个类来完成,这个就是org.apache.tomcat.util.http.mapper,这个类保存了Tomcat的Container容器中的所有子容器的信息。

当org.apache.catalina.connector.Request类在进入container容器之前,mapper会根据这次请求的hostname和contextPath,将host和context容器设置到request的mappingData属性中。

图9. request的mapper类关系图

servlet工作原理之tomcat篇_第9张图片

图10. request在容器中的路由图

servlet工作原理之tomcat篇_第10张图片

上图描述了一次request请求是如何达到最终的wrapper容器的,请求到达最终的servlet还要完成一些步骤,必须要执行filter链、以及要通知你在web.xml中定义的listener。接下去就要执行servlet的service方法。

当servlet从容器中移除时,也就表明servlet的生命周期结束了,这时servlet的destroy方法将被调用。

Session与cookie

servlet能够给我们提供两部分数据,一个是在servlet初始化时调用init方法时设置的ServletConfig,这个类基本上含有了servlet本身和servlet所运行的容器的基本信息。还有一部分是由ServiceRequest类提供,它的实际对象是RequestFacade,从提供的方法中发现主要是描述这次请求的http协议的信息。

session与cookie的作用都是为了保持访问用户与后端服务器的交互状态。服务器通过session id创建HttpSession对象,第一次触发是通过request.getSession方法,如果当前的session id还没有对应的HttpSession对象那就创建一个新的,并将这个对象加到org.apache.catalina.Manager的session容器中保存,Manager类将管理所有Session的生命周期,Session过期将被回收,服务器关闭,Session将被序列化到磁盘。只要这个HttpSession对象存在,用户就可以根据session id来获取到这个对象,也就达到了状态的保持。

图11. Session相关类图

servlet工作原理之tomcat篇_第11张图片

从上图可以看出request.getSession中获取的HttpSession对象是StandardSession对象的门面对象,这与前面的Request和Servlet是一样的原理。

图12. Session工作的时序图

servlet工作原理之tomcat篇_第12张图片

servlet中的listener

listener的设计对开发servlet应用程序提供了一种快捷的手段,能够方便地从另一个纵向维度控制程序和数据。目前servlet中提供了5种两类事件的观察者接口,它们分别是:4个EventListeners类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener;和2个LifecycleListener类型的,ServlectContextListener、HttpSessionListener。

图13. servlet中的listener

servlet工作原理之tomcat篇_第13张图片

这些listener的实现类可以配置在web.xml中的标签中,也可以在应用程序中动态添加。

你可能感兴趣的:(技术专题)