Tomcat(二) Tomcat实现:
Servlet与web.xml介绍 以及 源码分析Tomcat实现细节
在《Tomcat(一) Tomcat是什么:Tomcat与Java技术 Tomcat与Web应用 以及Tomcat基本框架及相关配置》对Tomcat有了一个大体的认识,下面将深入了解Tomcat技术的实现:
1、先来了解JavaEE Servlet技术的一些对象组件;
2、再来了Web应用程序部署文件web.xml中对Servlet组件的定义;
3、最后再从Tomcat源码分析一些实现细节,重点关注:Tomcat的启动/初始化、并发线程模式、接收请求与处理、以及Servlet容器的实现。
1、JavaEE Servlet技术
Tomcat是一个Servlet容器,实现了Servlet规范,可以运行我们自己编写的Servlet应用程序处理动态请求,并返回响应,下面介绍Listener、Filter、Servlet、Request、Respones这几个比较常见的对象元素。
1、Listener
Listener(监听器)用来监听一些对象的事件,当事件发生时可以进入到定义监听器中进行处理,主要有ServletContextListener、HttpSessionListener 和 ServletRequestListener。
我们程序中可以实现这些JavaEE Servlet API中提供的监听器接口,然后通过web.xml定义部署发生作用,如ServletContextListenerServlet(Servlet 上下文事件监听器)接口可以监听到部署web应用Servlet的状态改变通知(如创建、关闭),如Spring框架中常见的一些ServletContextListenerServlet实现在web.xml定义如下:
它们都实现javax.servlet.ServletContextListener接口,如org.springframework.web.context.ContextLoaderListener用来在部署相关Web应用时初始化其上下文环境,在停止退出Web应用时清理其资源,如下:
Servlet容器需要在开始执行进入应用的第一个请求之前完成Web应用中的监听器类的实例化,而且保持每一个监听器的引用直到为Web应用最后一个请求提供服务。
更多Listener信息请参考:《Servlet3.1规范(最终版)》第10章
2、Filter
Filter(过滤器)可以改变HTTP请求的内容、响应、及header信息,过滤器通常不产生响应或像 servlet 那样对请求作出响应,而是修改或调整到资源的请求,修改或调整来自资源的响应。
Filter在部署描述符中通过<filter>元素声明,一个过滤器或一组过滤器可以通过在部署描述符中定义<filter-mapping>来为调用配置。
容器部署Web应用时,必须确保它为过滤器列表中的每一个都实例化了一个适当类的过滤器, 并调用其Filter.init()方法;当容器接收到传入的请求时,它将获取列表中所有符合该请求的过虑器组成过虑器链(FilterChain),然后通过FilterChain嵌套调用各过虑器的Filter.doFilter()方法(一个过滤器可以使得滤器链调用下一个过滤器),传入ServletRequest 和ServletResponse进行处理。
当滤器链调用完各Filter.doFilter()方法后,会调用相关Servlet.service()方法,从而进行Servlet的处理。注意,Filter.doFilter()可以阻止过滤器链接下来的调用,返回时过滤器负责填充Reponse对象。
过滤器实例移除之前,容器必须先调用过滤器的Filter.destroy()方法。
如Struts2框架通过在web.xml中定义其实现了Filter接口的StrutsPrepareAndExecuteFilter来作为处理请求的入口,如下:
StrutsPrepareAndExecuteFilter.init()在部署Web应用时被容器调用,其实现了Struts2配置文件的读取及初始化,而在doFilter()方法中找到符合的Action处理程序后,就不再调用chain.doFilter(),而经Action处理后开始返回,如下:
更多Filter信息请参考:《Servlet3.1规范(最终版)》第6章
3、Servlet
Servlet 这里指的是实现了javax.servlet.Servlet接口的处理程序,一般开发中都是继承HttpServlet,其继承自实现了Servlet接口的GenericServlet类。
容器在部署Web应用程序时,或Servlet第一次请求处理时实例化调用Servlet.init()方法;而Servlet.service()方法从上面Filter介绍中可以知道是在各Filter.doFilter()嵌套调用中被调用的,而HttpServlet..service()中会根据请求类型分为doGet(),doPost()等方法,继承HttpServlet只需重写这样些需要的方法即可,如Tomcat的/conf/web.xml定义了默认的DefaultServlet用来处理静态内容请求(如html/js/各种图片),如下:
而当Servlet容器确定servlet应该从服务中移除时(内存不足等),将调用Servlet.destroy()方法以允许Servlet释放它使用的任何资源和保存任何持久化的状态。
容器中的Servlet实例通常都是单例的(可通过实现SingleThreadModel 接口实现每个请求对应一个Servlet),所以需要注意在多线程下的线程安全问题。
更多Servlet信息请参考:《Servlet3.1规范(最终版)》第2章
4、Request与Response
Request和Response都可以在上面这些处理接口程序中通过参数接收到,Request对象封装了客户端请求的所有信息,Response对象封装了从服务器返回到客户端的所有信息。
每个Request/Response对象只在Servlet.service()方法、或Filter.doFilter()方法的作用域内有效;发生异步处理的情况下,对象一直有效,直到调用AsyncContext.complete()方法。
除了异步请求时的AsyncContext.startAsync()和AsyncContext.complete()方法,Request/Response对象的实现都不保证线程安全,多线程访问时需要同步处理。
更多信息请参考:《Servlet3.1规范(最终版)》
2、web.xml文件说明
上篇文件章介绍过web.xml是Web应用程序部署描述符文件web.xml,描述组成应用程序的servlet和其他组件、以及相关初始化参数等信息。
web.xml中组件加载顺序为:
context-param -> listener -> filter -> servlet(同类则按编写顺序执行)。
web.xml常用组件解析:
- <span style="color:#000000;"><web-app>
- <display-name></display-name> <!--WEB应用的名字 -->
- <description></description> <!--WEB应用的描述-->
- <context-param></context-param> <!--context-param元素声明应用范围内的初始化参数-->
- <!--例如指定spring加载多个spring配置文件-->
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- /WEB-INF/applicationContext.xml, /WEB-INF/action-servlet.xml
- </param-value>
- </context-param>
- <filter></filter> <!--过滤器将一个名字与一个实现javax.servlet.Filter接口的类相关联-->
- <filter-mapping></filter-mapping><strong> <</strong>!--一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联-->
- <listener></listener> <!--事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类,如Log4j这个广泛使用的监听器-->
- <servlet></servlet> <!--在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。-->
- <servlet-mapping></servlet-mapping> <!--服务器一般为servlet提供一个缺省的URL:http://host/webAppPrefix/servlet/ServletName,但是,常常会更改这个URL,以便servlet可以访问初始化参数或更容易地处理相对URL。在更改缺省URL时,使用servlet-mapping元素-->
- <session-config></session-config> <!--如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存,可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值-->
- <mime-mapping></mime-mapping> <!--如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证-->
- <welcome-file-list></welcome-file-list> <!--指示服务器在收到引用一个目录名而不是文件名的URL时,使用哪个文件(其实就是欢迎界面或者说入口界面一般为index.*) -->
- <error-page></error-page> <!--在返回特定HTTP状态代码时,或者特定类型的异常被抛出时,能够制定将要显示的页面-->
- <taglib></taglib> <!--对标记库描述符文件(Tag Libraryu Descriptor file)指定别名。此功能使你能够更改TLD文件的位置, 而不用编辑使用这些文件的JSP页面-->
- <resource-env-ref></resource-env-ref> <!--声明与资源相关的一个管理对象-->
- <resource-ref></resource-ref> <!--声明一个资源工厂使用的外部资源-->
- <security-constraint></security-constraint> <!--制定应该保护的URL。它与login-config元素联合使用-->
- <login-config></login-config> <!--指定服务器应该怎样给试图访问受保护页面的用户授权,它与sercurity-constraint元素联合使用-->
- security-role></security-role> <!--给出安全角色的一个列表,这些角色将出现在servlet元素内的security-role-ref元素的role-name子元素中。分别地声明角色可使高级IDE处理安全信息更为容易-->
- <env-entry></env-entry> <!--声明Web应用的环境项-->
- </web-app></span>
更多部署文件信息请参考:《Servlet3.1规范(最终版)》第14章
另外从Servlet3.0开始可以支持一些注释直接在代码中进行定义部署,如下图中,并不需要web.xml也能运行该Servlet:
3、Tomcat一些实现细节
上面大体了解Servlet技术与web.xml,下面来了解Tomcat的一些具体实现细节,从一些关注点入手:Tomcat的启动/初始化配置、Tomcat如何实现Servlet容器--处理请求并响应等。
下面是以Tomcat 8.5.9版本源码调试分析,不同版本可能有差异;另外,图片看不清可以另外打开查看。
3-1、Tomcat启动/初始化
3-1-1、catclina.shstart命令启动
Tomcat的项目工程名称也称为"catclina",启动是通常是通过"catclina.sh start"脚本命令(start.sh也是调用该命令),调用了JDK中的Java虚拟机来运行"org.apache.catalina.startup.Bootstrap"的main()方法入口,脚本内容如下:
3-1-2、设置类加载器/Tomcat的类加载器架构
先是Bootstrap.initClassLoaders()设置类加载器:CommonClassLoader 、ServerClassLoader 、SharedClassLoader,分别对应的加载目录为(根目录下):/common(存放Tomcat与所有Web应用程序共用的类库)、/server(只Tomcat使用、而所有Web应用程序不可见的)、/shared(Tomcat不可见、而所有Web应用程序共用)。
这是通过/conf/catalina.properties设置,默认只为设置了CommonLoader,所以只会创建CommonLoader,而Server.loader 、SharedLoader都使用CommonLoader,而对应的三个目录合并为/lib(这是Tomcat6的简化改进)。
另外,在后面初始化每个Web应用程序解析web.xml时,会创建WebappClassLoader,只有对应的Web应用程序可见,加载对应Web应用程序的/WEB-INF/lib里的类库。
所以默认情况下,Tomcat类加载器架构如下:
其中Bootstrap类加载器为Java虚拟机提供,包含JDK基本运行时类,而System类加载器用于Tomcat启动初始化(通常忽略);另外,Tomcat类加载器架构是按照经典的"双亲委派模型"来实现的,即:当类加载器被要求加载特定的类或资源时,它首先将请求委托给父类加载器,然后只有当父类加载器找不到请求的类或资源时,它才在自己的存储库中查找。
Tomcat的类加载器架构的好处是可以按需要实现Tomcat与Web应用程序、以及不现Web应用程序之间的类库共享与隔离,如常用的Spring等类库可以放到共享目录,为多个Web应用程序共用;而"双亲委派模型"也是JDK类加载器的架构,可以有效组织类库的层次结构,避免一个类被不同加载器加载多次(注意,同一个类文件被不同加载器加载表示不同的类)。
更多JVM类加载器信息请参考:《Java虚拟机规范》第5章 加载、链接与初始化
更多Tomcat类加载器信息请参考:Tomcat Doc《Class Loader HOW-TO》
3-1-3、解析server.xml,创建各Tomcat组件
如图,Catalina.load()中创建Diegster实例后,用该实例解析Tomcat的核心配置文件server.xml,然后根据server.xml文件中的配置规则开始创建各个组件,如Server、Connector等等,源码中主要过程如下:
创建Connector是根据配置的协议找到对应的处理器类名,然后通过反射来创建该处理器,如下:
3-1-4、初始化各Tomcat组件
各组件创建后,Catalina.load()中调用Server组件的初始化函数,Server.initInternal()又调用内部包含的Service组件的初始化…以此类推,按配置文件的组件结构顺序初始化,如Http类型的Connector组件初始化会对其ServerSocket进行绑定,源码过程如下:
3-1-5、启动各Tomcat组件
上图只展示了开始的一部分过程,同上面初始化一样,是按照各组件在配置文件中的组织结构来启动,从Catalina.start()开始,而在Service中分别启动Engine和Connector,还需要注意的是MapperLinstener.start()的处理,如下:
(1)、Engine.start()--部署Web应用程序
(A)、启动部署,创建Context组件
其中Engine.startInternal()会为每个Host创建一个部署线程,Host在部署线程中调用HostConfig.deployApps()部署三种定义的Web应用程序,如下:
同样,如果发现需要部署的Web应用程序,会为每个Web应用程序创建一个单独的部署线程,然后在该线程中创建相应的Context组件,并在Context.startInternal()中创建相应的WebApp类加载器,如下:
(B)、解析web.xml
而后开始在ContextConfig.webConfig()中解析web.xml部署描述文件,在ContextConfig.configureContext(WebXml webxml)根据解析web.xml的信息创建封装各种应用元素,如Filter、Servlet、Session等等,其中Servlet被封装成Wrapper,加入到相应的Context组件中,如下:
(C)、Servlet.init()初始化
接下来就是Servlet实例初始化,Servlet规范中定义可以通过"load-on-startup"参数来指定,不小于0的整数时加载后调用Servlet.init(),定义多个Servlet时越大越先初始化;而小于0或没定义该参数时由容器实现来定义,Tomcat中除JspServlet外,其他的Servlet类型是在第一次使用处理请求时再调用Servlet.init(),/conf/web.xml文件中默认的两个Servlet都定义了"load-on-startup"参数大于0,在加载后调用Servlet.init(),如下:
DefaultServlet.init()调用过程如下:
而处理请求时调用的Servlet.init()过程如下:
(D)、Web应用程序变化监控
Host部署完成全部的Web应用程序后,退回到main线程,创建一个后台线程,在Engine容器的生命周期中每隔10秒会监控检查各Web应用程序内容(war文件的时间戳、context.xml、web.xml)是否已更改,如果改变会自动重新加载,创建过程如下:
(2)、MapperListener.start()—建立URL映射关系
解析完web.xml得到封装相关信息的各对象后,通过MapperListener建立配置的URL映射关系,即保存到Service中的一个"org.apache.catalina.mapper.Mapper"实例里,主要是Host、Contex、 Wrappers(Servlet)与对应的URL的映射关系,当处理用户请求时,可以很方便的通过请求URL从该Mapper实例中找到对应的Host、Contex、 Wrappers(Servlet)来处理。
建立映射关系的主要过程如下:
(3)、Connect.start()—创建并发线程模型
前面说到了Connector和相应处理器的创建,而Connect.start()主要是创建并发线程模型来接收处理请求,下面以默认的Http/1.1 NIO类型的Connector为例说明,它会创建四种类型的线程:Worker线程、Poller线程、Acceptor线程、AsyncTimeout线程,前三种整体创建过程如下:
(A)、创建Work线程
Work(工作)/exec线程用来处理请求连接,先根据配置参数创建Work线程池,线程池最小和最大线程数量可以通过"minSpareThreads"和"maxThreads"设置,默认为最小10和最大200,而线程空闲时间默认为60s后关闭,源码中创建过程如下:
(B)、创建Poller线程,并启动
说到Poller(轮询)线程就得提下Java NIO,通过Selector来实现Socket I/O多路复用模型:当Acceptor线程中ServerSocket.accept()监听到一个新的请求连接时,会把表示该请求连接的Socket注册到Poller线程的Selector中,Selector可以阻塞监听多个请求连接的Socket,当其中有Socket发生监听事件(如请求数据到来)时会退出阻塞状态,然后把发生请求数据事件的Socket交给Work线程处理。
而Poller(轮询)线程用来轮询Selector,这样的好处就是:在没有请求数据时,只有极少数的Poller线程阻塞,而避免了直接使用多个Work线程阻塞的消耗。
创建过程如(前三种线程整体创建过程)中的图,其中Poller线程数量可以通过参数"pollerThreadCount"设置,默认值为每个处理器1个,但不超过2个,当大量的发送文件操作正在进行时,增加该值也可能是有益的。
关于更多的I/O模型信息请参考:《5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO》
(C)、创建Acceptor线程,并启动
上面说到Acceptor线程用来调用ServerSocket.accept()(8080端口)阻塞监听到用户新的请求连接,创建过程如下:
(D)、创建AsyncTimeout线程,并启动
AsyncTimeout线程是用来实现异步请求的超时,创建过程如下:
关于Http/1.1 NIO类型的Connector创建完成后如下图(选中线程,而Work线程在接收请求数据时创建):
3-2、Tomcat请求处理过程
上面介绍了Tomcat的启动过程,主要初始化了各组件、Servlet,以及创建4种类型线程的并发模型,其中Work线在接收请求数据时再来创建。
下面还是以HTTP NIO类型的Connector为例,先介绍各类型线程间具体是如何接收请求的,再先来介绍请求处理过程。
3-2-1、各类型线程间接收请求/并发模型
1、Acceptor线程接收请求,创建连接Socket
在浏览器中访问Tomcat默认ROOT应用下的tomcat.png ,URL为:http://localhost:8080/tomcat.png;然后Acceptor线程中ServerSocket.accept()就会监听到这个新的请求连接,返回一个代表该请求连接的socket,如下:
2、连接Socket注册到Poller线程Selector进行监听
然后为该Socke创建一个NIO Channel(通道),该通道有许多特性,操作Socket是通过该通道进行的;然后把该通道注册到其中的一个Poller线程(这里是Poller1)的Selector多路复用器中,如下:
3、Selector监听到数据,创建/交给Work(Exec)线程处理
如上图,NioEndpoint.Poller.register()注册函数里会插入一个"OP_READ"操作(Key),这使得Poller线程中的Selector返回该Key进行处理,然后Poller线程中先创建一个SocketProcessor处理器,然后再通过线程池创建Work(Exec)线程并启动处理来该Socket,如下:
3-2-2、请求处理过程
上面说到了请求如何传递到Work(Exec)线程的,下面主要介绍在Work线程运行SocketProcessor来处理来该Socket,整体过程如下:
1、提取请求信息到Request对象,找到URI映射关系
先是创建一个Http11Processor,再用该处理器来处理,如下
同样 Http11Processor和CoyoteAdapter主要是都提取请求信息,封装到Request对象,后面的处理都是通过Request对象来处理,而后通过Response对象返回响应。
其中需要注意的是,CoyoteAdapter.postParseRequest()方法中会通过前面介绍Tomcat启动时保存Host、Context、Wapper(Servlet)映射关系的Mapper实例,找到该URL请求对应的Host、Context、Wapper(Servlet),并保存到Request对象的MappingData成员对象中,如下:
2、Tomcat组件对请求的处理
之后就是通过该Request对象的MappingData找到相应的一系列组件来处理的过程,整个过程都是以责任链模式来处理的。
3、Filter过虑器对请求处理
接下主要关注StandardWrapper中Servlet的处理,先为该Servlet创建一个FilterChain,并把Web.xml中定义的对该Servlet匹配的Filter(过虑器)加入到该FilterChain,如下:
这是就是FilterChain.doFilter()调用Filter.doFilter()来处理请求(),在每个Filter.doFilter()又会调用FilterChain.doFilter(),这样以责任链的模式不断嵌套调用,直到FilterChain.doFilter()发现所有Filter都调用完成,再调用Servlet的处理函数servlet.service()。
4、Servlet程序对请求处理,返回静态资源
如请求中会有一个WsFilter过滤器,然后调用到默认的DefaultServlet,过程如下:
默认的DefaultServlet用来处理静态内容请求(如html/js/各种图片),由于继承自JavaEE中的HttpServlet,所以HttpServlet.service()中会根据GET请求类型调用到DefaultServlet中重写的doGet()方法,该方法只调用实例自己的serveResource()方法,如下:
所以serveResource()方法是实现静态资源请求处理的过程,如果请求资源不存在CacheEntry.exists为false,则返回404(SC_NOT_FOUND),如下:
如果客户端存在缓存,checkIfHeaders()会根据客户端请求头的If-None-Match,If-Modified-Since等,来判断请求资源是否被修改,如果未被修改,则返回304头,客户端直接从客户端缓存中读取资源文件,如下:
否则,先设置一些Response Http头的字段,如contentType,而后读取请求的图片数据,如下:
最后返回,释放相关资源,可以看到请求图片如下:
到这里,我们对Tomcat的Servlet技术实现一个基本的认识,后面将进行配置Tomcat+nginx+keepalived的动静分离、会话保持的高可用集群……