美文网首页Tomcat
通过Tomcat”高层“看Tomcat的启动过程

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

作者: 胡飞飞_1995 | 来源:发表于2019-06-08 10:33 被阅读0次

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

    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 <code>Server</code> 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<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子容器来处理,具体是通过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的启动过程

        本文链接:https://www.haomeiwen.com/subject/tqtixctx.html