写点什么

servlet 工作原理之 tomcat 篇

用户头像
hasWhere
关注
发布于: 2021 年 06 月 16 日

原文:https://blog.csdn.net/guzhangyu12345/article/details/91047750

servlet 容器

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

图 1. tomcat 容器模型



servlet 容器的启动过程

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

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

图 2 tomcat 主要类的启动时序图:


当 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<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。这里 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

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

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

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

图 4. 初始化 servlet 的时序图



servlet 体系结构

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

图 5. servlet 顶层类关联图



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

ServletConfig

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

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

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



上图可以看出 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 相关类结构图



上图是 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 如何工作

当用户从浏览器向服务器发起一个请求,通常会包含如下信息: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 类关系图



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



上图描述了一次 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 相关类图



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

图 12. Session 工作的时序图



servlet 中的 listener

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

图 13. servlet 中的 listener



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

用户头像

hasWhere

关注

间歇性努力的学习渣 2018.04.20 加入

通过博客来提高下对自己的要求

评论

发布
暂无评论
servlet工作原理之tomcat篇