架构篇:Tomcat 高层组件构建一个商业帝国

简介: 架构篇:Tomcat 高层组件构建一个商业帝国

Tomcat 架构解析到设计思想借鉴中我们学到 Tomcat 的总体架构,学会从宏观上怎么去设计一个复杂系统,怎么设计顶层模块,以及模块之间的关系;

Tomcat 实现的 2 个核心功能:

  • 处理 Socket 连接,负责网络字节流与 RequestResponse 对象的转化。
  • 加载并管理 Servlet ,以及处理具体的 Request 请求。

所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container),连接器负责对外交流,容器负责内部处理。

Tomcat整体架构

本篇作为 Tomcat 系列的第三篇,带大家体会 Tomcat 帝国是如何构建的?高层组件如何管理组件的?连接器容器是如何被启动和管理的?

Tomcat 启动流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()

Tomcat 启动流程

Bootstrap、Catalina、Server、Service、 Engine 都承担了什么责任?

单独写一篇介绍他们是因为你可以看到这些启动类或者组件不处理具体请求,它们的任务主要是管理管理下层组件的生命周期并且给下层组件分配任务,也就是把请求路由到负责干活儿的组件。

他们就像一个公司的高层,管理整个公司的运作,将任务分配给专业的人。

我们在设计软件系统中,不可避免的会遇到需要一些管理作用的组件,就可以学习和借鉴 Tomcat 是如何抽象和管理这些组件的。

因此我把它们比作 Tomcat 的高层,同时愿干活的不再 996

对了,因为微信更改了推送规则,推文不再按照时间线显示,如果不想错过我的文章,请把公众号设置『星标』,经常点赞,评论也可以防止失联,以及支持鼓励我持续更新。

Bootstrap

当执行 startup.sh 脚本的时候,就会启动一个 JVM 运行 Tomcat 的启动类 Bootstrapmain 方法。

先看下他的成员变量窥探核心功能:

public final class Bootstrap {
    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;

它的主要任务就是初始化 Tomcat 定义的类加载器,同时创建 Catalina 对象

Bootstrap 就像一个大神,初始化了类加载器,加载万物。

关于为何自定义各种类加载器详情请查看码哥的 Tomcat 架构设计解析 类加载器部分。

初始化类加载器

WebAppClassLoader

假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。

Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例

我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context容器负责创建和维护一个 WebAppClassLoader加载器实例

这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。

Tomcat 的自定义类加载器 WebAppClassLoader打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到则通过 ExtClassLoader 加载 JRE 核心类防止黑客攻击,无法加载再代理给 AppClassLoader 加载器,其目的是优先加载 Web 应用自己定义的类。

具体实现就是重写 ClassLoader的两个方法:findClassloadClass

SharedClassLoader

假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加载到内存后,Tomcat要保证这两个 Web 应用能够共享,也就是说 Spring的 JAR 包只被加载一次。

SharedClassLoader 就是 Web 应用共享的类库的加载器,专门加载 Web 应用共享的类。

如果  WebAppClassLoader自己没有加载到某个类,就会委托父加载器 SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

CatalinaClassloader

如何隔离 Tomcat 本身的类和 Web 应用的类?

要共享可以通过父子关系,要隔离那就需要兄弟关系了。

兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。

这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?

老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader和  SharedClassLoader 的父加载器。

CommonClassLoader能加载的类都可以被  CatalinaClassLoaderSharedClassLoader 使用。

Catalina

Tomcat 是一个公司,Catalina 就好像是一个创始人。因为它负责组建团队,创建 Server 以及所有子组件。

Catalina 的主要任务就是创建 Server,解析 server.xml 把里面配置的各个组件创建出来,并调用每个组件的 initstart方法,将整个 Tomcat 启动,这样整个公司就在正常运作了。

我们可以根据 Tomcat 配置文件来直观感受下:

<Server port="8005" shutdown="SHUTDOWN"> // 顶层组件,可包含多个 Service,代表一个 Tomcat 实例
  <Service name="Catalina">  // 顶层组件,包含一个 Engine ,多个连接器
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />  // 连接器
 // 容器组件:一个 Engine 处理 Service 所有请求,包含多个 Host
    <Engine name="Catalina" defaultHost="localhost">
   // 容器组件:处理指定Host下的客户端请求, 可包含多个 Context
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
   // 容器组件:处理特定 Context Web应用的所有客户端请求
   <Context></Context>
      </Host>
    </Engine>
  </Service>
</Server>

作为创始人,Catalina 还需要处理公司的各种异常情况,比如有人抢公章(执行了 Ctrl + C 关闭 Tomcat)。

Tomcat 要如何清理资源呢?

通过向 JVM 注册一个「关闭钩子」,具体关键逻辑详见

org.apache.catalina.startup.Catalina#start 源码:

  1. Server 不存在则解析 server.xml 创建;
  2. 创建失败则报错;
  3. 启动 Server;
  4. 创建并注册「关闭钩子」;
  5. await 方法监听停止请求。
/**
     * Start a new server instance.
     */
    public void start() {
        // 如果 Catalina 持有的 Server 为空则解析 server.xml 创建
        if (getServer() == null) {
            load();
        }
        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }
        // Start the new server
        try {
            getServer().start();
        } catch (LifecycleException e) {
            // 省略部分代码
        }
        // 创建钩子并注册
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
        // 监听停止请求,内部调用 Server 的 stop
        if (await) {
            await();
            stop();
        }
    }

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

它其实就是一个线程,当 JVM 停止前尝试执行这个线程的 run 方法。

org.apache.catalina.startup.Catalina.CatalinaShutdownHook

protected class CatalinaShutdownHook extends Thread {
        @Override
        public void run() {
            try {
                if (getServer() != null) {
                    Catalina.this.stop();
                }
            } catch (Throwable ex) {
              // 省略部分代码....
            }
        }
    }

其实就是执行了 Catalina 的 stop 方法,通过它将整个 Tomcat 停止。

Server

Server 组件的职责就是管理 Service 组件,负责调用持有的 Servicestart 方法。

他就像是公司的 CEO,负责管理多个事业部,每个事业部就是一个 Service

它管理两个部门:

  • Connector 连接器:对外市场营销部,推广吹牛写 PPT 的。
  • Container 容器:研发部门,没有性生活的 996

实现类是 org.apache.catalina.core.StandardServer,Server 继承 org.apache.catalina.util.LifecycleMBeanBase,所以他的生命周期也被统一管理,Server 的子组件是 Service,所以还需要管理 Service 的生命周期。

也就是说在启动和关闭 Server 的时候会分别先调用 Service 的 启动和停止方法。

这就是设计思想呀,抽象出生命周期 Lifecycle 接口,体现出接口隔离原则,将生命周期的相关功能内聚。

我们接着看 Server 如何管理 Service 的,核心源码如下org.apache.catalina.core.StandardServer#addService:

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。

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

在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。

Service

他的职责就是管理 Connector 连接器顶层容器 Engine,会分别调用他们的 start 方法。至此,整个 Tomcat 就算启动完成了。

Service 就是事业部的话事人,管理两个职能部门对外推广部(连接器),对内研发部(容器)。

Service 组件的实现类是org.apache.catalina.core.StandardService,直接看关键的成员变量。

public class StandardService extends LifecycleMBeanBase 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);

继承 LifecycleMBeanBase 而  LifecycleMBeanBase 又继承 LifecycleBase,这里实际上是模板方法模式的运用,org.apache.catalina.util.LifecycleBase#initorg.apache.catalina.util.LifecycleBase#startorg.apache.catalina.util.LifecycleBase#stop 分别是对应的模板方法,内部定义了整个算法流程,子类去实现自己内部具体变化部分,将变与不变抽象出来实现开闭原则设计思路。

那为什么还有一个 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. 最后启动连接器,连接器会启动它子组件,比如 Endpoint
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

这里启动顺序也很讲究,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。

这很好理解,因为内层组件启动好了才能对外提供服务,产品没做出来,市场部也不能瞎忽悠,研发好了才能启动外层的连接器组件。

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

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

Engine

他就是一个研发部的头头,是最顶层的容器组件。继承 Container,所有的容器组件都继承 Container,这里实际上运用了组合模式统一管理。

他的实现类是 org.apache.catalina.core.StandardEngine,继承 ContainerBase

public class StandardEngine extends ContainerBase implements Engine {
}

他的子容器是 Host,所以持有 Host 容器数组,这个属性每个容器都会存在,所以放在抽象类中

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

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

org.apache.catalina.core.ContainerBase#startInternal

// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
  results.add(startStopExecutor.submit(new StartChild(child)));
}

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

容器组件最重要的功能是处理请求,而 Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。

每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),透过构造方法创建 Pipeline。

public StandardEngine() {
    super();
    pipeline.setBasic(new StandardEngineValve());
    // 省略部分代码
}

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 中的第一个 Valve
      host.getPipeline().getFirst().invoke(request, response);
  }
}

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

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

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

重磅!程序员交流群已成立公众号运营至今,离不开小伙伴们的支持。为了给小伙伴们提供一个互相交流的平台,特地开通了程序员交流群群里有不少技术大神,不时会分享一些技术要点,更有一些资源收藏爱好者不时分享一些优质的学习资料。(群完全免费,不广告不卖课!)需要进群的朋友,可长按扫描下方二维码回复「加群」。▲长按扫码


相关文章
|
3天前
|
敏捷开发 监控 数据管理
构建高效微服务架构的五大关键策略
【4月更文挑战第20天】在当今软件开发领域,微服务架构已经成为一种流行的设计模式,它允许开发团队以灵活、可扩展的方式构建应用程序。本文将探讨构建高效微服务架构的五大关键策略,包括服务划分、通信机制、数据管理、安全性考虑以及监控与日志。这些策略对于确保系统的可靠性、可维护性和性能至关重要。
|
15天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
18 0
|
3天前
|
消息中间件 监控 持续交付
构建高效微服务架构:后端开发的进阶之路
【4月更文挑战第20天】 随着现代软件开发的复杂性日益增加,传统的单体应用已难以满足快速迭代和灵活部署的需求。微服务架构作为一种新兴的分布式系统设计方式,以其独立部署、易于扩展和维护的特点,成为解决这一问题的关键。本文将深入探讨微服务的核心概念、设计原则以及在后端开发实践中如何构建一个高效的微服务架构。我们将从服务划分、通信机制、数据一致性、服务发现与注册等方面入手,提供一系列实用的策略和建议,帮助开发者优化后端系统的性能和可维护性。
|
14天前
|
Kubernetes 安全 Java
构建高效微服务架构:从理论到实践
【4月更文挑战第9天】 在当今快速迭代与竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性及容错性,成为众多企业转型的首选。本文将深入探讨如何从零开始构建一个高效的微服务系统,覆盖从概念理解、设计原则、技术选型到部署维护的各个阶段。通过实际案例分析与最佳实践分享,旨在为后端工程师提供一套全面的微服务构建指南,帮助读者在面对复杂系统设计时能够做出明智的决策,并提升系统的可靠性与维护效率。
|
1天前
|
监控 API 持续交付
构建高效微服务架构:后端开发的新趋势
【4月更文挑战第23天】 随着现代软件开发实践的不断演进,微服务架构已经成为企业追求敏捷、可扩展和弹性解决方案的首选。本文深入探讨了如何构建一个高效的微服务架构,涵盖了关键的设计原则、技术选型以及实践建议。通过分析微服务的独立性、分布式特性和容错机制,我们将揭示如何利用容器化、服务网格和API网关等技术手段,来优化后端系统的可维护性和性能。文章旨在为后端开发人员提供一套全面的指南,以应对不断变化的业务需求和技术挑战。
|
2天前
|
Cloud Native API 持续交付
构建未来:云原生架构在企业数字化转型中的关键作用
【4月更文挑战第21天】 随着企业加速其数字化转型的步伐,云原生技术已迅速成为推动创新和实现敏捷性的基石。本文深入探讨了云原生架构的核心组件,包括容器化、微服务、持续集成/持续部署(CI/CD)以及声明式API。通过分析这些技术的协同效应,揭示了它们如何共同促进系统的可伸缩性、弹性和维护性,进而支持企业在不断变化的市场环境中保持竞争力。
10 1
|
2天前
|
敏捷开发 Cloud Native 持续交付
构建未来:云原生架构的进化之路
【4月更文挑战第21天】随着数字化转型的深入,企业对IT基础设施的要求日益提高。云原生技术以其灵活性、可扩展性和敏捷性成为推动创新的重要力量。本文将探讨云原生架构的核心组件,分析其如何助力企业实现快速迭代和高效运营,并预测云原生技术的发展趋势。
|
5天前
|
监控 持续交付 开发者
构建高效微服务架构:后端开发的新趋势
【4月更文挑战第18天】在数字化转型的浪潮中,微服务架构已成为企业提升系统灵活性、加速产品迭代的关键。此文深入探讨了构建高效微服务架构的实践方法,包括服务划分原则、容器化部署、持续集成/持续部署(CI/CD)流程以及监控与日志管理等关键技术点。通过分析具体案例,揭示了微服务在提高开发效率、降低维护成本及促进团队协作方面的显著优势。
|
5天前
|
Cloud Native 持续交付 云计算
构建未来:云原生架构在企业数字化转型中的关键作用
【4月更文挑战第18天】 随着企业加速迈向数字化,云原生架构成为推动创新与效率的催化剂。本文深入探讨了云原生技术如何助力企业实现敏捷开发、自动化运维和无缝可扩展性,以及它如何塑造着云计算的未来。我们将通过具体案例分析,揭示云原生架构在处理复杂系统时的灵活性和可靠性,并展望其对业务连续性和安全性的积极影响。
10 1
|
8天前
|
Cloud Native 持续交付 API
构建未来:云原生架构在企业数字化转型中的关键作用
【4月更文挑战第15天】 随着企业加速其数字化转型的步伐,云原生架构已经成为推动创新和实现敏捷性的关键技术。本文深入探讨了云原生技术如何助力企业在竞争激烈的市场中保持领先地位,包括它的核心组件、实施策略以及面临的挑战。通过实际案例分析,我们揭示了企业如何利用云原生架构来优化资源使用、提高开发效率和加强系统的稳定性与安全性。