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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容