Tomcat 通过 server.xml 配置文件装配一系列组件, 并且为组件设计生命周期接口, 在容器启停时, 协调控制组件的启动, 初始化和停止. 容器通常使用脚本启动, 脚本主要是检查 Java 环境, 设置 JVM 参数, 调用 Bootstrap.start 启动.
Bootstrap 是 Catalina 的引导加载类, 它构造了一个 commonLoader 类加载器, 加载
${catalina.base}/lib
目录下的类, 目的是与应用程序级的类隔离, 接下来详细分析每个过程.
在此之前可先了解一下, 官网对启动过程的描述, 以及提供的启动 UML 序列图, Startup.txt&UML http://tomcat.apache.org/tomcat-6.0-doc/architecture/startup.html , 这个图囊括了启动过程中类的调用, 但我第一次看这个图, 也是一脸懵, 以下描述的则是通过 DEBGUG 跟出来的, 当回过头再看这个图, 确实很有用.
初始化
初始化涉及到 server.xml 的解析, Tomcat 使用 Digester 解析, 其工作原理理解了很简单, 使用 sax 解析, 在元素开始和结束, 借助一个 ObjectStack 对象栈和一系列解析规则完成组件的初始化, 它主要有三个基本规则:
ObjectCreateRule: 根据指定的 ClassName 创建一个实例, 元素开始时, 压入对象栈, 结束时, 弹出对象栈
SetPropertiesRule: 元素开始时, 根据其属性, 反射调用栈顶元素对应成员变量的 set 方法
SetNextRule: 元素结束时, 使用 set 方法建立栈顶元素 (child) 和(top-1)元素 (parent) 的父子 (组合) 关系
各组件都是使用这三个规则进行配置解析, 解析过程不再赘述,(这里) https://github.com/rmwheel/rw-tomcat/tree/master/tomcat-6.0.53 提供一份源码注释, 可加断点跟一下, 着重关注栈内对象的入栈和出栈, 值得注意的是在 GlobalResourcesLifecycleListener 初始化时会触发加载解析 mbeans-descriptors.xml, 这个过程太长, 可使用断点跳过.
调用 StandardServer.initialize 开始组件的初始化, 触发 INIT_EVENT 事件, 详细过程:
初始化 StandardServer: 首先触发各 Listener(具体有哪些, 可查看 xml 的配置)的执行, 然后初始化内部的 Services;
Service 主要是初始化定义的 Connectors;
初始化 Connector: 初始化 Adapter 和 ProtocolHandler, 处理器有两种不同实现:
Http11NioProtocol 初始化: NioEndpoint 设置线程池名称, 设置 ConnectionHandler, 设置接收和发送 ByteBuffer 容量; SSL 相关实现初始化:
初始化 NioEndpoint: 初始化 ServerSocketChannel, 设置成阻塞模式, 绑定端口; 设置 Acceptor,Poller 线程数目; 初始化 SSL 信息; 初始化 NioSelectorPool;
Http11Protocol 初始化: JIoEndpoint 设置线程池名和 ConnectionHandler, 初始化 ServerSocketFactory:
初始化 JioEndpoint: 设置 Acceptor 线程数; 创建 ServerSocket 并绑定端口.
初始化完毕
以上就是在组件在 init 生命周期事件中完成的设置, 注意在 Digester 解析过程中, 也完成了一系列的设置.
启动
调用 StandardServer.start 开启组件的启动过程, 触发 BEFORE_START_EVENT,START_EVENT,AFTER_START_EVENT 事件:
启动 StandardServer: 触发执行各 Listener; 启动内部的 Services;
Service 默认没有 Listener, 首先启动 Engines , 接着启动 Executors, 最后启动 Connectors;
启动 Engine, 它调用 super.start() 进行启动或触发以下的动作:
尝试启动 Manager,Cluster,Realm(LockOutRealm);
启动子容器 Hosts;
EngineConfig 监听器的执行 - START_EVENT,STOP_EVENT;
启动后台线程, 定期检查会话超时.
启动 Host: 设置 ErrorReportValve, 并添加到自己的 pipeline 中, 触发 ADD_VALVE_EVENT 容器事件, 调用 super.start():
启动子容器 Contexts;
启动 pipeline 中实现 Lifecycle 的 Valve.
触发 HostConfig 监听器的执行:
PERIODIC_EVENT: 检查所有 web 应用程序的状态;
START_EVENT: 创建 Context, 启动并部署 webapps & conf/Catalina/localhost/*.xml;
STOP_EVENT: 取消已部署的所有应用.
启动 Context, 部署 Web App
根据 context.xml(或默认的)使用 Digester 创建 StandardContext 对象, 添加 ContextConfig 监听器, 通过 host.addChild 启动 Context;
初始化用于解析 web.xml 的 Digester, 创建设置 WebappLoader 且不使用 "标准委托模型";
先解析默认的 conf/web.xml, 然后处理 WEB-INF/web.xml, 创建 StandardWrapper 封装 Servlet.
启动 Connector, 无 Listener, 它主要是启动 ProtocolHandler:
Http11NioProtocol - 启动 NioEndpoint - 启动 Poller 线程, 启动 Acceptor 线程;
Http11Protocol - 启动 JioEndpoint - 启动 Acceptor 线程.
注册 ShutdownHook, 阻塞 main 线程, 启动完毕.
接下来就该处理请求了, 这部分下一篇会介绍.
停止
当调用 Bootstrap.stop 或接收到 SHUTDOWN 指令或者捕捉到系统关闭信号 (如 ctrl+c,shutdown,logoff) 时, 开始调用 Catalina.stop 方法, 关闭组件, 触发 BEFORE_STOP_EVENT,STOP_EVENT 事件: 停止 Server,Service, 暂停 Connector, 停止内部组件, 停止 Connector, 关闭线程池, 打断 awaitThread 即 main 线程, 结束.
如果 stop 命令执行失败, 尝试使用系统信号终止, 首先使用 kill -15 , 进程收到后进行处理; 如果还是失败那么, 等待 5s 后, 使用 kill -9 强制终止.
小结
当我看到 Web 应用加载的时候, 耐心有点不足, 感觉捋顺了整个过程, 其实还有很多地方经不起推敲, 为了看得更透彻, 那么这个 "简单" 的启动过程, 又有哪些值得思考的呢?
内部启动的线程和线程池中的线程为什么设置为守护线程(Daemon)?
Java 中有两类线程: 守护线程与用户线程, 区别是守护线程不会阻止 JVM 的退出. 从 Tomcat 的停止流程来看, 就算用非守护线程也不会出现什么问题, 没有搜到官方对此的描述, 这里按照理解强行解释一波 :).
守护线程一般是服务提供者, 运行系统代码, 比如 GC 线程, 就像操作系统中也区分用户线程和内核线程那样, Tomcat 将内部线程设为 daemon, 也能很好的在语义上区分 Servlet 启动的线程, 并且对于 Servlet 来说 Tomcat 就是它的操作系统.
生命周期的设计有什么好处?
保证组件启动和停止的一致性, 为生命周期事件添加监听器, 这些监听器处理其感兴趣的事件, 来做一些额外的操作. 这是观察者模式的应用.
多应用间如何实现隔离?
应用隔离的本质就是类隔离, 主要防止类冲突. 类是否相等是由其全限定名和类加载器共同决定的, 容器就是通过自定义 ClassLoader 实现应用间隔离, Tomcat 类加载器结构:
当要求类加载器加载类时, 它首先将请求委托给父加载器, 然后在父加载器找不到所请求的类时, 查找自己的存储库. 而 Webapp 加载器略有不同, 它首先会在自己的资源库中搜索, 而不是向上委托, 打破了标准的委托机制, 其类加载时按以下顺序查找资源库:
Bootstrap 和 System 已加载的类
/WEB-INF/classes 和 /WEB-INF/lib/*.jar
Common 已加载的类
应用热加载和热部署?
当在启动 Engine 时, 会新建一个名为 ContainerBackgroundProcessor[StandardEngine[Catalina]] 的线程, 默认 10s 检查是否要重新加载或重新部署, 对应方法在 Loader 接口定义分别是 backgroundProcess 和 modified.
默认 web.xml 配置了什么?
提供一个 DefaultServlet, 用于处理静态资源和未找到匹配 Servlet 的请求;
用于编译和执行 JSP 的 JspServlet;
Session 默认超时 30 minutes;
默认 MIME Type 映射和 Welcome Files
其他能够想到的点
采用都是常用的数据结构, 如 ArrayList, 数组, HashMap 等, Pipeline 采用链表实现, Digester 使用栈来解析. 欢迎补充.
来源: https://www.cnblogs.com/wskwbog/p/9362647.html