Tomcat 启动时组件做了什么

如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

  试用 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 方法。

  这样我们的 Tomcat 就完成了启动。

Catalina

  Catalina 是启动类,它的主要任务就是上面我们提到的解析 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.启动Server
    try {
    
    
        getServer().start();
    } catch (LifecycleException e) {
    
    
        return;
    }

    //创建并注册关闭钩子
    if (useShutdownHook) {
    
    
        if (shutdownHook == null) {
    
    
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    //用await方法监听停止请求
    if (await) {
    
    
        await();
        stop();
    }
}

  那什么是关闭钩子呢?它又是做什么的呢?其实它是一个线程,JVM 在停止之前会尝试调用整个线程的 run 方法。下面我们来看看整个钩子CatalinaShutdownHook做了什么?

protected class CatalinaShutdownHook extends Thread {
    
    

    @Override
    public void run() {
    
    
        try {
    
    
            if (getServer() != null) {
    
    
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
    
    
           ...
        }
    }
}

  从代码上看,实际就是调用了 Server 的 stop 方法,Server 的 stop 方法会释放和清理所有的资源。

Server 组件

 &emspp;Server 组件的具体实现类是 StandardServer。它继承了 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 中,这就是典型的观察者模式

  作为“管理”角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。我们来看看 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 组件

  最后是 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 在启动 Host 子容器时就直接重用了这个方法。那 Engine 自己做了什么呢?我们知道容器组件最重要的功能是处理请求,而 Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的,就是我们之前学过的 Pipeline Valve。

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 容器直接从 request 中拿到了,这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。

总结

  当我们在设计系统或者框架时,也要考虑两个方面:

  1. 首先要选用合适的数据结构来保存子组件,比如 Server 用数组来保存 Service 组件,并且采取动态扩容的方式,这是因为数组结构简单,占用内存小;再比如 ContainerBase 用 HashMap 来保存子容器,虽然 Map 占用内存会多一点,但是可以通过 Map 来快速的查找子容器。因此在实际的工作中,我们也需要根据具体的场景和需求来选用合适的数据结构。
  2. 其次还需要根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。这正是“管理者”应该考虑的事情。

猜你喜欢

转载自blog.csdn.net/qq_43654226/article/details/127267473