Tomcat的“高层们”

Hi,我是阿昌,今天学习记录的关于Tomcat中针对启动过程管理和加载的一系列类的功能

Tomcat 的/bin目录下的脚本startup.sh来启动 Tomcat,那你是否知道我们执行了这个脚本后发生了什么呢?

下面这张流程图来了解一下:

  1. Tomcat 本质上是一个 Java 程序,因此startup.sh脚本会启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。
  2. Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。
  3. Catalina 是一个启动类,它通过解析server.xml、创建相应的组件,并调用 Server 的 start 方法。
  4. Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法。
  5. Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。

可以把 Bootstrap 看作是上帝,它初始化了类加载器,也就是创造万物的工具。

如果我们把 Tomcat 比作是一家公司,那么 Catalina 应该是公司创始人,因为 Catalina 负责组建团队,也就是创建 Server 以及它的子组件。

Server 是公司的 CEO,负责管理多个事业群,每个事业群就是一个 Service。

Service 是事业群总经理,它管理两个职能部门:

  • 一个是对外的市场部,也就是连接器组件;
  • 另一个是对内的研发部,也就是容器组件。

Engine 则是研发部经理,因为 Engine 是最顶层的容器组件。你可以看到这些启动类或者组件不处理具体请求,它们的任务主要是“管理”,管理下层组件的生命周期,并且给下层组件分配任务,也就是把请求路由到负责“干活儿”的组件。


一、Catalina

Catalina 的主要任务就是创建 Server,它不是直接 new 一个 Server 实例就完事了,而是需要解析server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用 Server 组件的 init 方法和 start 方法,这样整个 Tomcat 就启动起来了。

作为“管理者”,Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”。

public void start() {//1. 如果持有的Server实例为空,就解析server.xml创建出来if (getServer() == null) {load();}//2. 如果创建失败,报错退出if (getServer() == null) {log.fatal(sm.getString("catalina.noServer"));return;}//3.启动Servertry {getServer().start();} catch (LifecycleException e) {return;}//创建并注册关闭钩子if (useShutdownHook) {if (shutdownHook == null) {shutdownHook = new CatalinaShutdownHook();}Runtime.getRuntime().addShutdownHook(shutdownHook);}//用await方法监听停止请求if (await) {await();stop();}
}

那什么是“关闭钩子”,它又是做什么的呢?

如果我们需要在 JVM 关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向 JVM 注册一个“关闭钩子”。

“关闭钩子”其实就是一个线程,JVM 在停止之前会尝试执行这个线程的 run 方法。

Tomcat 的“关闭钩子”CatalinaShutdownHook 做了些什么。

protected class CatalinaShutdownHook extends Thread {@Overridepublic void run() {try {if (getServer() != null) {Catalina.this.stop();}} catch (Throwable ex) {...}}
}

Tomcat 的“关闭钩子”实际上就执行了 Server 的 stop 方法,Server 的 stop 方法会释放和清理所有的资源。


二、Server 组件

Server 组件的具体实现类是 StandardServer,我们来看下 StandardServer 具体实现了哪些功能。

Server 继承了 LifecycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。

Server 在内部维护了若干 Service 组件,它是以数组来保存的,那 Server 是如何添加一个 Service 到数组中的呢?

@Override
public void addService(Service service) {service.setServer(this);synchronized (servicesLock) {//创建一个长度+1的新数组Service results[] = new Service[services.length + 1];//将老的数据复制过去System.arraycopy(services, 0, results, 0, services.length);results[services.length] = service;services = results;//启动Service组件if (getState().isAvailable()) {try {service.start();} catch (LifecycleException e) {// Ignore}}//触发监听事件support.firePropertyChange("service", null, service);}}

从上面的代码你能看到,它并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。

除此之外,Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,这就是为什么你能通过 shutdown 命令来关闭 Tomcat。

不知道你留意到没有,上面 Catalina 的启动方法的最后一行代码就是调用了 Server 的 await 方法

在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;

如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。


三、Service 组件

Service 组件的具体实现类是 StandardService,看看它的定义以及关键的成员变量。

public class StandardService extends LifecycleBase implements Service {//名字private String name = null;//Server实例private Server server = null;//连接器数组protected Connector connectors[] = new Connector[0];private final Object connectorsLock = new Object();//对应的Engine容器private Engine engine = null;//映射器及其监听器protected final Mapper mapper = new Mapper();protected final MapperListener mapperListener = new MapperListener(this);

StandardService 继承了 LifecycleBase 抽象类,此外 StandardService 中还有一些我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。

那为什么还有一个 MapperListener?这是因为 Tomcat 支持热部署,当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式

作为“管理”角色的组件,最重要的是维护其他组件的生命周期。

此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。我们来看看 Service 启动方法:

protected void startInternal() throws LifecycleException {//1. 触发启动监听器setState(LifecycleState.STARTING);//2. 先启动Engine,Engine会启动它子容器if (engine != null) {synchronized (engine) {engine.start();}}//3. 再启动Mapper监听器mapperListener.start();//4.最后启动连接器,连接器会启动它子组件,比如Endpointsynchronized (connectorsLock) {for (Connector connector: connectors) {if (connector.getState() != LifecycleState.FAILED) {connector.start();}}}
}

从启动方法可以看到,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。

这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。

而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。

组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。


四、Engine 组件

容器组件 Engine 具体是如何实现的。

Engine 本质是一个容器,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口

public class StandardEngine extends ContainerBase implements Engine {}

我们知道,Engine 的子容器是 Host,所以它持有了一个 Host 容器的数组,这些功能都被抽象到了 ContainerBase 中,ContainerBase 中有这样一个数据结构:

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBase 用 HashMap 保存了它的子容器,并且 ContainerBase 还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如 ContainerBase 会用专门的线程池来启动子容器。

for (int i = 0; i < children.length; i++) {results.add(startStopExecutor.submit(new StartChild(children[i])));
}

所以 Engine 在启动 Host 子容器时就直接重用了这个方法。

那 Engine 自己做了什么呢?我们知道容器组件最重要的功能是处理请求,而 Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。

每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:

final class StandardEngineValve extends ValveBase {public final void invoke(Request request, Response response)throws IOException, ServletException {//拿到请求中的Host容器Host host = request.getHost();if (host == null) {return;}// 调用Host容器中的Pipeline中的第一个Valvehost.getPipeline().getFirst().invoke(request, response);}}

这个基础阀实现非常简单,就是把请求转发到 Host 容器。

你可能好奇,从代码中可以看到,处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?

这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。


五、总结

由启动类和“高层”组件来完成的,它们都承担着“管理”的角色,负责将子组件创建出来,并把它们拼装在一起,同时也掌握子组件的“生杀大权”。

所以当我们在设计这样的组件时,需要考虑两个方面:

  • 首先要选用合适的数据结构来保存子组件,比如 Server 用数组来保存 Service 组件,并且采取动态扩容的方式,这是因为数组结构简单,占用内存小;
  • 再比如 ContainerBase 用 HashMap 来保存子容器,虽然 Map 占用内存会多一点,但是可以通过 Map 来快速的查找子容器。

因此在实际的工作中,我们也需要根据具体的场景和需求来选用合适的数据结构。

其次还需要根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。这正是“管理者”应该考虑的事情。


Day675.Tomcat的“高层们” -深入拆解 Tomcat Jetty相关推荐

  1. mysql性能调优与架构设计_了解架构设计远远不够!一文拆解 Tomcat 高并发原理与性能调优

    来源 | 码哥字节 上帝视角拆解 Tomcat 架构设计,在了解整个组件设计思路之后.我们需要下凡深入了解每个组件的细节实现.从远到近,架构给人以宏观思维,细节展现饱满的美.关注「码哥字节」获取更多硬 ...

  2. 了解架构设计远远不够!一文拆解 Tomcat 高并发原理与性能调优

    来源 | 码哥字节 上帝视角拆解 Tomcat 架构设计,在了解整个组件设计思路之后.我们需要下凡深入了解每个组件的细节实现.从远到近,架构给人以宏观思维,细节展现饱满的美.关注「码哥字节」获取更多硬 ...

  3. 《深入拆解Tomcat Jetty》笔记

    极客时间<深入拆解Tomcat & Jetty>笔记 03_你应该知道的Servlet规范和Servlet容器 1.HTTP服务器怎么知道要调用哪个Java类的哪个方法呢.最直接的 ...

  4. tomcat 连接oracle重连,JSP+Tomcat连接Oracle数据库

    1.   首先安裝JDK   1.4.2_01   ,http://java.sun.com/j2se/2.   接著安裝Oracle9i,   安裝完成之後先更改XML   Database的參數 ...

  5. 配置Tomcat监听80端口 配置Tomcat虚拟主机 Tomcat日志

    配置Tomcat监听80端口 • vim /usr/local/tomcat/conf/server.xml Connector port="8080" protocol=&quo ...

  6. 用tomcat插件 在Eclipse 中配置Tomcat项目

    1.安装Tomcat在Eclipse中的插件(使可以在eclipse中启动Tomcat) 2.培植eclipse中tomcat属性, window-->preferences 对话框中Tomca ...

  7. Tomcat学习总结(2)——Tomcat使用详解

    2019独角兽企业重金招聘Python工程师标准>>> 一.Tomcat服务器端口的配置 Tomcat的所有配置都放在conf文件夹之中,里面的server.xml文件是配置的核心文 ...

  8. tomcat 启动项目 页面文字乱码_eclipse启动tomcat项目乱码而终端启动tomcat正常的解决办法...

    部署在eclipse上的web程序访问会出现乱码,只要是涉及到中文增删改,string转码都会出现乱码,eclipse上使用的是tomcat服务器,数据库是oracle.而 直接使用tomcat部署程 ...

  9. IntelliJ IDEA启动Tomcat后,却无法访问Tomcat主页

    IntelliJ IDEA启动Tomcat后,却无法访问Tomcat首页? 如下: 可以访问web项目 却无法访问Tomcat主页!!!! 为什么呢? 我们的项目的工件输出目录在图一红框框出来的地方, ...

最新文章

  1. 确定安全威胁与漏洞-A
  2. 模板方法模式(Template Pattern)
  3. 【Android NDK 开发】JNI 引用 ( 局部引用 | 局部引用作用域 | 局部引用产生 | 局部引用释放 | 代码示例)
  4. Rxjava+Retrofit+Mvp的使用实例(基于retrofit2.1.0)
  5. c语言排序算法 应用与实现,基于C语言排序算法改进与应用.doc
  6. GStreamer(一)
  7. Python学习(三)基础
  8. linux 我的世界 跨平台联机,我的世界跨平台联机 PC、手机等平台数据互通
  9. android系统语音合成,android 语音合成报错
  10. Java实现XSS防御
  11. 分享一些自己的学习过程和学习方法
  12. 这又是什么新玩法?华为Mate 30 Pro真机谍照现身:音量键大变样
  13. linux ssd硬盘做缓存,linux系统中ssd当块设备缓存
  14. 西部动力成功中标围场县政府网站群建设项目
  15. Spring Boot 集成 Spring Security 实现权限认证模块
  16. Sprite Editor 图集切片精灵
  17. excel如何拟合直线(怎样用excel拟合直线)
  18. 无法找到 :import javax.annotation.Resource-------Resource 这个注解
  19. git中fatal: Authentication failed for 的问题
  20. 连续8个季度增长超100% 阿里云成长为“亚洲巨象”

热门文章

  1. jQuery dataTables 网格
  2. redis的list存储对象实现
  3. 看了就知道世纪佳缘为什么能够上市了,这个市场的确够大啊
  4. 获取输入汉字的中文读音
  5. 贪吃蛇java代码_java实现贪吃蛇的代码实例
  6. word导航栏部分标题内容不显示问题
  7. python的文件操作
  8. sobel算子 matlab实现6,sobel算子,matlab实现
  9. 应用sobel算子算法c语言,Canny算子与Sobel算子求图像边缘的C代码实现
  10. 关于soap 的PHP客户端调用