通过Tomcat”高层“看Tomcat的启动过程

我们可以通过Tomcat的/bin目录下的脚本startup.sh来启动Tomcat,执行了这个脚本会发生什么呢? 通过下面这张流程图了解一下。

通过Tomcat”高层“看Tomcat的启动过程_第1张图片
Tomcat启动流程图.jpg
  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方法。

Catalina

Catalina的主要任务就是创建Server,需要解析出server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用Server组件的init方法和start方法,这样整个Tomcat就启动起来了。作为”管理者“,Catalina还需要处理各种异常情况,比如我们通过”Ctrl + C“关闭Tomcat时,Tomcat将如何优雅的停止并且清理资源呢?因此Catalina在JVM中注册了一个”关闭钩子“。

    public void start() {
        // 如果持有的Server实例为空,就解析server.xml创建一个
        if (getServer() == null) {
            load();
        }
        // 如果创建失败 报错退出
        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // 启动Server
        try {
            getServer().start();
        } catch (LifecycleException e) {
            log.fatal(sm.getString("catalina.serverStartFail"), e);
            try {
                getServer().destroy();
            } catch (LifecycleException e1) {
                log.debug("destroy() failed for failed Server ", e1);
            }
            return;
        }

        long t2 = System.nanoTime();
        if(log.isInfoEnabled()) {
            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
        }

        // 创建并注册JVM关闭钩子
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }
        // 用await方法监听停止请求
        if (await) {
            await();
            stop();
        }
    }

那什么是”关闭钩子“,它又是做什么的呢?如果我们需要在JVM关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向JVM注册一个”关闭钩子“,”关闭钩子“其实就是一个线程,JVM在停止之前会尝试执行这个线程的run方法。下面是Tomcat的”关闭钩子“CatalinaShutdownHook:

    protected class CatalinaShutdownHook extends Thread {

        @Override
        public void run() {
            try {
                if (getServer() != null) {
                    Catalina.this.stop();
                }
            } catch (Throwable ex) {
                ExceptionUtils.handleThrowable(ex);
                log.error(sm.getString("catalina.shutdownHookFail"), ex);
            } finally {
                // If JULI is used, shut JULI down *after* the server shuts down
                // so log messages aren't lost
                LogManager logManager = LogManager.getLogManager();
                if (logManager instanceof ClassLoaderLogManager) {
                    ((ClassLoaderLogManager) logManager).shutdown();
                }
            }
        }
    }

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

Server组件

Server组件的具体实现类是StandardServer,Server继承了LifecycleBase,它的生命周期被统一管理,并且它的子组件是Service,因此它还要管理Service的生命周期,也就是说在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,下面是Server添加一个Service到数组中的方法:

public void addService(Service service) {
        service.setServer(this);
        synchronized (servicesLock) {
            // 创建一个长度加一的数组
            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
                }
            }
            // 触发监听事件
            // Report this property change to interested listeners
            support.firePropertyChange("service", null, service);
        }
    }

除此之外,Server组件还有一个重要的任务是启动一个Socket类监听停止端口,这就是为什么你能通过shutdown命令来关闭Tomcat。上面Caralina的启动方法的最后一行代码就是调用了Server的await方法。在await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就新建连接,然后从Socket中读取数据;如果读到的数据是停止命令”SUTDOWN“,就退出循环,进入stop流程。

Service组件

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

public class StandardService extends LifecycleMBeanBase implements Service {
    /**
     * The name of this service. 
     * Service的名字
     */
    private String name = null;
    
    /**
     * The Server that owns this Service, if any.
     * Server实例
     */
    private Server server = null;
    
    /**
     * The set of Connectors associated with this Service.
     * 连接器数组
     */
    protected Connector connectors[] = new Connector[0];
    private final Object connectorsLock = new Object();
    
    // 对应的Engine容器
    private Engine engine = null;
    
    /**
     * Mapper.
     * 映射器
     */
    protected final Mapper mapper = new Mapper();
    
    /**
     * Mapper listener.
     * 映射器的监听器
     */
    protected final MapperListener mapperListener = new MapperListener(this);
}

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

作为”管理“角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序,Service的启动方法:

    protected void startInternal() throws LifecycleException {
        if(log.isInfoEnabled())
            log.info(sm.getString("standardService.start.name", this.name));
            
        // 触发启动监听器
        setState(LifecycleState.STARTING);
        
        // 先启动engine, Engine会启动它的子容器
        // Start our defined Container first
        if (engine != null) {
            synchronized (engine) {
                engine.start();
            }
        }
   
        synchronized (executors) {
            for (Executor executor: executors) {
                executor.start();
            }
        }
        
        // 启动Mapper容器
        mapperListener.start();
        
        // 启动连接器,连接器会启动它的子组件 比如Endpoint
        // Start our defined Connectors second
        synchronized (connectorsLock) {
            for (Connector connector: connectors) {
                try {
                    // If it has already failed, don't try and start it
                    if (connector.getState() != LifecycleState.FAILED) {
                        connector.start();
                    }
                } catch (Exception e) {
                    log.error(sm.getString(
                            "standardService.connector.startFailed",
                            connector), e);
                }
            }
        }
    }

从启动方法可以看到,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 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子容器来处理,具体是通过Valve来实现的。

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

final class StandardEngineValve extends ValveBase {

    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Select the Host to be used for this Request
        // 拿到请求中的Host容器
        Host host = request.getHost();
        if (host == null) {
            response.sendError
                (HttpServletResponse.SC_BAD_REQUEST,
                 sm.getString("standardEngine.noHost",
                              request.getServerName()));
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // Ask this Host to process this request
        // 调用Host容器中的Pipeline中的第一个Valve
        host.getPipeline().getFirst().invoke(request, response);
    }
}

这个基础阀实现非常简单,就是把请求转发到Host容器。我们可以看到处理请求的Host容器对象是从请求中拿到的,请求对象中怎么会有Host容器呢?这是因为请求到达Engine容器之前,Mapper组件已经对请求进行了路由处理,Mapper组件通过请求的URL定位了相应的容器,并且把容器对象保存到了请求对象中。

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

你可能感兴趣的:(通过Tomcat”高层“看Tomcat的启动过程)