000. 小念头
为了学习一些开源的软件, 自己第一个念头就是去阅读该软件的源码, 于是就选择tomcat拿来练练手. 因为tomcat工作中使用到的机会挺多的, 这也是想去深入了解的一个原因.本文是个人的阅读源码后的一个总体性的总结, 其中可能有些技术细节描述可能不到位, 希望读者能够指正出来, 不吝赐教, 当然也希望本文也能帮助那些准备学习tomcat的人.
001. 准备工作
在准备阅读源码时, 就想过怎么去阅读这个源码, 这么复杂, 有点没有头绪.
冥思苦想一番, 有了一点点线索了.
- 用idea本地启动一个本地tomcat的web项目,项目使用了spring mvc
1.1 用jvisualvm给启动的项目进行线程dump和堆dump保存
jvisualvm中dump出来的线程可以帮助我确定tomcat的入口函数, 堆dump可以帮助我分析tomcat中核心对象的组织结构. 大家有兴趣的可以自己dump下, 在本地观察下.
- 先去tomcat官网下载源码
2.1 用idea打开
用idea看源码比较方便,
idea提供了方法调用的层次路径(ctrl+alt+h),
引用的依赖查询(alt+f7),
类的结构窗口(alt+7)
浏览位置导航(ctrl+alt+向左方向箭头,ctrl+alt+向右)等等,
这些对我们看源码的时候帮助很大
准备工作完毕
010. 入口
根据线程的dump信息, 看到了一个名为main的线程, 根据这个线程的调用的堆栈信息迅速定位到了tomcat的入口方法.
"main" #1 prio=5 os_prio=0 tid=0x0000000002952800 nid=0x3a8c runnable [0x000000000251e000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
- locked <0x00000000820d2bf8> (a java.net.SocksSocketImpl)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.catalina.core.StandardServer.await(StandardServer.java:466)
at org.apache.catalina.startup.Catalina.await(Catalina.java:769)
at org.apache.catalina.startup.Catalina.start(Catalina.java:715)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:353)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:493)
最后一行就是tomcat的入口位置, 根据这个入口信息我们就可以开启tomcat的源码之路.
011. tomcat的内部流程
根据我阅读源码得到总结,tomcat运行时的流程, 总的来说分为两个大的流程:初始化和启动流程和http请求处理流程.下面就聊聊这两大流程是怎么进行的
tomcat的初始化和启动
tomcat核心对象的组织结构
在查看tomcat的初始化流程之前, 先看下Bootstrap的对象结构, 因为入口就在Bootstrap的, 所以这个类应该会有一些关键成员信息来支撑tomcat的流程的运转, 下面是我从 jvisualvm中截的图
在其中有几个成员看起来是不是很眼熟的感觉, 其中有一个成员catalinaDeamon, 这个成员对象是非常重要的, 实际上大多数tomcat中关键对象的初始化大都是在这个类中触发的.另外还有一个需要注意的类成员 classLoader, 可以看出Bootstrap类的类加载器是Launcher.AppClassLoader, 这是也我们在启动java应用时默认的类加载器, 但是从下面的对象类型开始, 这个类加载器就变了, 挖坑完毕. 在下面的介绍中, 只介绍tomcat中的关键服务容器对象, 这类对象都是用来承载tomcat的核心流程的, 好了进入下一个关键对象Catalina中去看看
看到没, Catalina对象的classLoader变成了URLClassLoader, 这就是改变了默认的Java程序双亲加载模型的关键点, TODO.另外可以看到catalina对象有一个类型为StandardServer的server成员, 这个对象也是tomcat的关键对象
StandardServer有一个类型为Service的services数组成员, 数组里面有一个StandardService的元素
在StandardEngine中, 类型为Map的成员children中包含了一个StandardHost对象,在后续的关键对象比如StandardHost.children是StandardContext,然后StandardContext.children 是StandardWrapper中实际上都是采用的这种父子关系来组织tomcat的服务容器之间的关系的.StandarHost后面的核心对象就不截图了
另外这些核心对象都有一个属性:lifecycleListener, 这个属性会在对象创建或者初始化的时候加入对应的监听器, 随后会在对象的各个生命周期的阶段:开始初始化,初始化完毕, 开始启动, 启动完毕, 关闭等这些阶段做相应的处理工作. 有一个类型为MapperListener的监听器对象, 这个监听器会被注册到所有的核心对象上, 这个对象主要是用来监听各个核心对象的生命周期事件, 然后去构造用来处理http请求流程的核心对象StandardService下的mapper属性, 这个mapper属性包含了如何从url路径映射到最终处理servlet流程中的所有关键信息.
tomcat核心对象的初始化和启动流程
"main" #1 prio=5 os_prio=0 tid=0x0000000002952800 nid=0x3a8c runnable [0x000000000251e000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
- locked <0x00000000820d2bf8> (a java.net.SocksSocketImpl)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.catalina.core.StandardServer.await(StandardServer.java:466)
进入方法Catalina.await后,就开启了处理请求的阶段
at org.apache.catalina.startup.Catalina.await(Catalina.java:769)
在这个方法Catalina.start调用进入下个调用Catalina.await等待接受处理请求之前
,都是处于初始化和启动阶段
at org.apache.catalina.startup.Catalina.start(Catalina.java:715)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
通过反射指定具体的类加载器URLClassLoader来实例化Catalina类
然后再通过反射调用Catalina.start就是为了使用URLClassLoader类加载器链
(URLClassLoader(tomcat/lib下的类)->AppClassLoader(tomcat/bin下的类)
->ExtClassLoader->BootStrap)
来加载Catalina类所直接或者间接依赖的所有类
, 除非其依赖的类使用了再次使用这样的指定类加载器的反射调用逻辑来改变类加载器链的起点,
否则所有依赖的类的加载器都是从URLClassLoader为起点开始查找的,在我们自定的Servlet类时,
比如Springmvc中的DispatherServlet时, tomcat会用另外一个类加载器ParallelWebappClassLoader来改变类加载器链的起点,
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:353)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:493)
初始化和启动流程开始于org.apache.catalina.startup.Catalina.start, 其中涉及两个关键的阶段:
- 初始化阶段
1.1 主要工作:
构造核心对象,初始化核心对象之间的结构关系
1.2 起点:
org.apache.catalina.startup.Catalina#start()
-> org.apache.catalina.startup.Catalina#load()
1.3 流程
1.3.1整体设计
:从Catalina.load这个方法所启动的流程主要是构造核心对象,核心对象的组织结构所使用的设计模式是组合模式
,这也就意味着其初始化流程会按照整体-部分形式进行, 只有当部分全部完成初始化, 整体才算初始化完毕. 这么设计的好处显而易见,一是符合标准web服务器的自身向外提供服务的树形组织形式(服务器,域名(host),站点应用...);而是自顶向下将具体功能逐层分解, 这样各层次只承担自身负责的主要功能, 不在自身范围内的就交给下一层处理, 下一层会选择如何进行后续流程的处理, 而上层只用暴露统一的外部入口.
1.3.2初始化顺序
:
1.3.2.1.Catalina.load中会根据server.xml中定义的核心对象进行对象的创建和组织,有一点需要知道的是这个阶段的核心对象只会构建到StandardHost这一层,StandardContext和StandardWrapper会在启动阶段进行; 另外会将一些监听器注册到对应的核心对象上,比如用于构建mapper的MapperListener,用于填充StandardEngine属性的EngineConfig等等.
1.3.2.2.然后进入真正的初始化方法调用, 由StandardServer.init为入口开始进行初始化, 初始化动作会一层层的执行, 直到最内层的完成初始化后,然后逐次向外完成各层的初始化动作,注意这块的初始化也是到StandardHost就结束了. - 启动阶段
2.1整体设计: 在核心对象整体完成初始化后,开始在启动阶段进行核心对象的关键属性填充,启动站点创建StandardContext,加载初始化Servlet类,以及MapperListener监听核心对象的生命周期事件来构造填充Mapper对象
2.2 起点
org.apache.catalina.startup.Catalina#start
-> org.apache.catalina.core.StandardServer#start
2.3 流程
启动由StandardServer.start为入口开始进行启动, 启动动作会一层层的执行, 直到最内层的完成启动后,然后逐次向外完成各层的启动动作, 在启动的过程中会触发核心对象的生命周期事件, 事件监听器就会在这个时候完成相关的流程; StandardHost的事件监听器HostConfig会在启动期间根据webapps下的站点目录进行创建StandardContext, 随后StandardContext的事件监听器ContextConfig还会根据context.xml和web.xml对StandardContext的关键属性进行初始化, 以及根据web.xml创建mapping,listener,servlet,filter等, 其中listener的监听事件处理的触发是在StandardContext启动过程中完成对servlet,mapping,filter这些基础组件的初始化后; 最后还有一个核心组件Connector对象的启动, 这个对象会启动http请求的监听流程Acceptor以及和http请求处理流程之间的对接流程Poller, Poller会将http请求转发给真正的处理流程对象.
请求处理流程
当初始化流程和启动流程完成后, tomcat就处于等待接受http请求的状态, 一旦有请求进来,tomcat就会开启新的请求处理流程来处理http请求.就是上一节中讲得Acceptor和Poller之间的交互流程.
Poller从将请求注册到Selector上, 然后从Selector获取已经准备就绪的请求,然后逐个处理就绪的请求
请求会被Http11Processor 进行处理, 进入这个处理器后, 会根据请求的地址和数据找到对应的映射信息(standardhost,standardcontext,standardwrapper ), 然后根据映射信息进入请求处理管道:StandardEngineValve-> StandardHostValve -> StandardContextValve->StandardWrapperValve.
进入StandardWrapperValve后会进入新的处理阶段, 找到请求对应的filter执行链, 执行filter的流程, 执行完后所有filter的逻辑后会进入servlet的处理. servlet执行结束后整个请求的处理流程就结束了, 最终响应数据会返回到客户端.
tomcat的扩展点
从源码分析看得出, tomcat内置提供的编程接口在三个地方
- Listener
在完成对servlet的初始化后, 会触发Listener的监听事件, 在这里我们可以进行一些全局化的设置工作. - Filter
在请求得到真正的servlet处理前, 做一些统一的处理操作, 比如权限验证, 日志记录等. - Servlet
处理请求, 输出响应结果的地方.