Tomcat啟動流程簡析

程式設計師自由之路發表於2020-07-04

Tomcat是一款我們平時開發過程中最常用到的Servlet容器。本系列部落格會記錄Tomcat的整體架構、主要元件、IO執行緒模型、請求在Tomcat內部的流轉過程以及一些Tomcat調優的相關知識。

力求達到以下幾個目的:

  • 更加熟悉Tomcat的工作機制,工作中遇到Tomcat相關問題能夠快速定位,從源頭來解決;
  • 對於一些高併發場景能夠對Tomcat進行調優;
  • 通過對Tomcat原始碼的分析,吸收一些Tomcat的設計的理念,應用到自己的軟體開發過程中。

1. Bootstrap啟動入口

在前面分析Tomcat啟動指令碼的過程中,我們最後發現startup.bat最後是通過呼叫Bootstrap這個類的main方法來啟動Tomcat的,所以先去看下Bootstrap這個類。

public static void main(String args[]) {

        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    //建立Bootstrap物件,代用init方法
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                //一般情況下會進入這步,呼叫Bootstrap物件的load和start方法。
                //將Catalina啟動設定成block模式
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }

上面的程式碼邏輯比較簡單,如果我們正常啟動tomcat,會順序執行Bootstrap物件的init()方法, daemon.setAwait(true)、daemon.load(args)和daemon.start()方法。我們先看下Bootstrap物件的init方法:

public void init() throws Exception {

        initClassLoaders();
        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();

        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;

    }

這個方法主要做了以下幾件事:

  • 建立commonLoader、catalinaLoader、sharedLoader類載入器(預設情況下這三個類載入器指向同一個物件。建議看看createClassLoader方法,裡面做的事情還挺多,比如裝載catalina.properties裡配置的目錄下的檔案和jar包,後兩個載入器的父載入器都是第一個,最後註冊了MBean,可以用於JVM監控該物件);
  • 例項化一個org.apache.catalina.startup.Catalina物件,並賦值給靜態成員catalinaDaemon,以sharedLoader作為入參通過反射呼叫該物件的setParentClassLoader方法。

執行完init()方法,就開始執行bootstrap物件的load和start方法;

 private void load(String[] arguments)
        throws Exception {

        // Call the load() method
        String methodName = "load";
        Object param[];
        Class<?> paramTypes[];
        if (arguments==null || arguments.length==0) {
            paramTypes = null;
            param = null;
        } else {
            paramTypes = new Class[1];
            paramTypes[0] = arguments.getClass();
            param = new Object[1];
            param[0] = arguments;
        }
        Method method =
            catalinaDaemon.getClass().getMethod(methodName, paramTypes);
        if (log.isDebugEnabled())
            log.debug("Calling startup class " + method);
        method.invoke(catalinaDaemon, param);

    }

呼叫catalinaDaemon物件的load方法,catalinaDaemon這個物件的型別是org.apache.catalina.startup.Catalina。strat方法也是類似的,最後都是呼叫Catalina的start方法。

總結下Bootstrap的啟動方法最主要乾的事情就是建立了Catalina物件,並呼叫它的load和start方法。

2. Catalina的load和start方法

第一節分析到Bootstrap會觸發呼叫Catalina的load和start方法。

     /**
     * 從註釋可以看出這個方法的作用是建立一個Server例項
     * Start a new server instance.
     */
    public void load() {
        if (loaded) {
            return;
        }
        loaded = true;
        long t1 = System.nanoTime();
        //檢查臨時目錄
        initDirs();
        // Before digester - it may be needed
        initNaming();

        // Create and execute our Digester
        Digester digester = createStartDigester();

        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
            try {
                file = configFile();
                inputStream = new FileInputStream(file);
                inputSource = new InputSource(file.toURI().toURL().toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail", file), e);
                }
            }
            if (inputStream == null) {
                try {
                    inputStream = getClass().getClassLoader()
                        .getResourceAsStream(getConfigFile());
                    inputSource = new InputSource
                        (getClass().getClassLoader()
                         .getResource(getConfigFile()).toString());
                } catch (Exception e) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("catalina.configFail",
                                getConfigFile()), e);
                    }
                }
            }

            // This should be included in catalina.jar
            // Alternative: don't bother with xml, just create it manually.
            if (inputStream == null) {
                try {
                    inputStream = getClass().getClassLoader()
                            .getResourceAsStream("server-embed.xml");
                    inputSource = new InputSource
                    (getClass().getClassLoader()
                            .getResource("server-embed.xml").toString());
                } catch (Exception e) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("catalina.configFail",
                                "server-embed.xml"), e);
                    }
                }
            }


            if (inputStream == null || inputSource == null) {
                if  (file == null) {
                    log.warn(sm.getString("catalina.configFail",
                            getConfigFile() + "] or [server-embed.xml]"));
                } else {
                    log.warn(sm.getString("catalina.configFail",
                            file.getAbsolutePath()));
                    if (file.exists() && !file.canRead()) {
                        log.warn("Permissions incorrect, read permission is not allowed on the file.");
                    }
                }
                return;
            }

            try {
                inputSource.setByteStream(inputStream);
                digester.push(this);
                digester.parse(inputSource);
            } catch (SAXParseException spe) {
                log.warn("Catalina.start using " + getConfigFile() + ": " +
                        spe.getMessage());
                return;
            } catch (Exception e) {
                log.warn("Catalina.start using " + getConfigFile() + ": " , e);
                return;
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

        // Stream redirection
        initStreams();

        // Start the new server
        try {
            getServer().init();
        } catch (LifecycleException e) {
            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
                throw new java.lang.Error(e);
            } else {
                log.error("Catalina.start", e);
            }
        }

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

將上面的程式碼精簡下:

Digester digester = createStartDigester();  
inputSource.setByteStream(inputStream);  
digester.push(this);  
digester.parse(inputSource);  
getServer().setCatalina(this);  
getServer().init();  

做的事情就兩個:

  • 建立一個Digester物件(Digester物件的作用就是解析server.xml配置檔案,這邊會先載入conf/server.xml檔案,找不到的話會嘗試載入server-embed.xml這個配置檔案),解析完成後生成org.apache.catalina.core.StandardServer、org.apache.catalina.core.StandardService、org.apache.catalina.connector.Connector、org.apache.catalina.core.StandardEngine、org.apache.catalina.core.StandardHost、org.apache.catalina.core.StandardContext等等一系列物件,這些物件從前到後前一個包含後一個物件的引用(一對一或一對多的關係)。最後將StandardServer賦值給Catalina物件的server屬性;如果你配置了聯結器元件共享的執行緒池,還會生成StandardThreadExecutor物件。
  • 第二件事就是呼叫StandardServer的init方法。

臨時總結下:Catalina的load方法的作用主要是解析conf/server.xml,生成StandardServer物件,再觸發StandardServer的init方法。

第一節中還分析到Bootstrap會觸發呼叫Catalina的start方法。那麼我們看看start方法中幹了什麼。

/**
     * Start a new server instance.
     */
    public void start() {

        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // Start the new 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");
        }

        // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);

            // If JULI is being used, disable JULI's shutdown hook since
            // shutdown hooks run in parallel and log messages may be lost
            // if JULI's hook completes before the CatalinaShutdownHook()
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }

        if (await) {
            await();
            stop();
        }
    }

這段程式碼最主要的作用就是呼叫StandardServer物件的start方法。

總結下:Catalina物件的laod和start方法的作用是解析conf/server.xml,生成StandardServer物件,再觸發StandardServer的init方法和start方法。

到這邊為止我們可以看到Tomcat的啟動流程還是很清晰的,下面繼續看StandardServer的init方法和start到底幹了些什麼。

3.StandardServer的init和start方法

通過尋找StandardServer的init方法,我們發現StandardServer本身沒有實現這個方法,這個方法是它從父類LifecycleBase中繼承過來的:

@Override
    public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        try {
            //釋出初始化容器時間,對應的listener做相應處理
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            //呼叫子類的initInternal()
            initInternal();
            //釋出容器已經初始化事件,對應的listener做相應處理
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.initFail", toString());
        }
    }

所以呼叫StandardServer的init方法,其實是促發了容器初始化事件釋出,然後又調到了StandardServer的initInternal方法。那麼我們看看StandardServer的start方法的邏輯是什麼。

程式碼點進去,發現StandardServer的start方法也是調的父類LifecycleBase中的方法。

@Override
    public final synchronized void start() throws LifecycleException {

        if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                LifecycleState.STARTED.equals(state)) {

            if (log.isDebugEnabled()) {
                Exception e = new LifecycleException();
                log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
            } else if (log.isInfoEnabled()) {
                log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
            }

            return;
        }

        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
            //釋出事件
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            //呼叫子類的startInternal
            startInternal();
            if (state.equals(LifecycleState.FAILED)) {
                // This is a 'controlled' failure. The component put itself into the
                // FAILED state so call stop() to complete the clean-up.
                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                // Shouldn't be necessary but acts as a check that sub-classes are
                // doing what they are supposed to.
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            } else {
                //釋出容器啟動事件
                setStateInternal(LifecycleState.STARTED, null, false);
            }
        } catch (Throwable t) {
            // This is an 'uncontrolled' failure so put the component into the
            // FAILED state and throw an exception.
            handleSubClassException(t, "lifecycleBase.startFail", toString());
        }
    }

從以上init和start方法的定義可以看到這兩個方法最終將會呼叫StandardServer中定義的initInternal和startInternal。

先來看initInternal方法

protected void initInternal() throws LifecycleException {

        super.initInternal();

        // Register global String cache
        // Note although the cache is global, if there are multiple Servers
        // present in the JVM (may happen when embedding) then the same cache
        // will be registered under multiple names
        onameStringCache = register(new StringCache(), "type=StringCache");

        // Register the MBeanFactory
        MBeanFactory factory = new MBeanFactory();
        factory.setContainer(this);
        onameMBeanFactory = register(factory, "type=MBeanFactory");

        // Register the naming resources
        globalNamingResources.init();

        // Populate the extension validator with JARs from common and shared
        // class loaders
        if (getCatalina() != null) {
            ClassLoader cl = getCatalina().getParentClassLoader();
            // Walk the class loader hierarchy. Stop at the system class loader.
            // This will add the shared (if present) and common class loaders
            while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
                if (cl instanceof URLClassLoader) {
                    URL[] urls = ((URLClassLoader) cl).getURLs();
                    for (URL url : urls) {
                        if (url.getProtocol().equals("file")) {
                            try {
                                File f = new File (url.toURI());
                                if (f.isFile() &&
                                        f.getName().endsWith(".jar")) {
                                    ExtensionValidator.addSystemResource(f);
                                }
                            } catch (URISyntaxException e) {
                                // Ignore
                            } catch (IOException e) {
                                // Ignore
                            }
                        }
                    }
                }
                cl = cl.getParent();
            }
        }
        // Initialize our defined Services
        for (int i = 0; i < services.length; i++) {
            services[i].init();
        }
    }

重點程式碼在最後,迴圈呼叫了Service元件的init方法。

再來看StandardServer的startInternal方法

@Override
    protected void startInternal() throws LifecycleException {

        fireLifecycleEvent(CONFIGURE_START_EVENT, null);
        setState(LifecycleState.STARTING);

        globalNamingResources.start();

        // Start our defined Services
        synchronized (servicesLock) {
            for (int i = 0; i < services.length; i++) {
                services[i].start();
            }
        }
    }

也是迴圈呼叫了Service元件的start方法。這邊的Service元件就是在從conf/server.xml中解析出來的StandardService物件,檢視下這個類的繼承體系:

LifecycleBase (org.apache.catalina.util)
    LifecycleMBeanBase (org.apache.catalina.util)
        StandardService (org.apache.catalina.core)

我們發現這個類繼承體系和StandardServer是一樣的。其實我們再觀察的仔細一點會發現從conf/server.xml解析胡來的類的繼承體系都是一樣的。所以我們呼叫這些類的init和start方法最後還是會呼叫到他們的initInternal和startInternal方法。

4. StandardService的initInternal和startInternal方法

先看下StandardService的initInternal方法

@Override
    protected void initInternal() throws LifecycleException {

        super.initInternal();
        //呼叫engine的initInternal方法,這個方法中也沒做特別重要的操作,只是做了一個getReal操作
        if (engine != null) {
            engine.init();
        }

        //StandardThreadExecutor的initInternal方法中沒沒幹什麼事情
        // Initialize any Executors
        for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }

        // Initialize mapper listener
        //這步也沒做什麼重要操作
        mapperListener.init();

        // Initialize our defined Connectors
        // 聯結器主鍵的初始化,主要是檢查聯結器的protocolHandler的主鍵,並將其初始化.
        synchronized (connectorsLock) {
            for (Connector connector : connectors) {
                connector.init();
            }
        }
    }

看了上面的程式碼,覺得Tomcat原始碼邏輯還是很清晰的。之前在分析Tomcat元件的文章中講到Service元件是有Connector元件、engine元件和一個可選的執行緒池組成。上面的程式碼中正好對應了這三個元件的初始化話。

Connector元件和engine元件的初始化又會觸發他們各自子元件的初始化,所以StandardService的initInternal方法會觸發Tomcat下各類元件的初始化。這邊大致記錄下各個元件初始化話的順序:

  • engine元件初始化:engine元件初始化沒做什麼特別的操作,也沒觸發它的子元件(Host、Context和Wrapper元件的初始化),所以這步比較簡單;
  • Executor元件的初始化:沒有觸發其他元件初始化;
  • Mapper元件初始化:mapper元件初始化也沒幹什麼重要的操作,也沒觸發其他子元件初始化;
  • Connector元件初始化:檢查聯結器的protocolHandler的子元件,並觸發其初始化
  • ProtocolHandler元件初始化:觸發Endpoint元件初始化,Endpoint類才是接收轉化請求的真正的類;

然後再看StandardService的startInternal方法

protected void startInternal() throws LifecycleException {

        if(log.isInfoEnabled())
            log.info(sm.getString("standardService.start.name", this.name));
        setState(LifecycleState.STARTING);

        // Start our defined Container first
        if (engine != null) {
            synchronized (engine) {
                engine.start();
            }
        }

        synchronized (executors) {
            for (Executor executor: executors) {
                executor.start();
            }
        }

        mapperListener.start();

        // Start our defined Connectors second
        synchronized (connectorsLock) {
            for (Connector connector: connectors) {
                // If it has already failed, don't try and start it
                if (connector.getState() != LifecycleState.FAILED) {
                    connector.start();
                }
            }
        }
    }

邏輯依然很清楚,StandardService會依次觸發各個子元件的start方法。

  • Engine元件的start:Engine元件的start方法組要作用還是觸發了Host元件的start方法,具體程式碼見

    protected synchronized void startInternal() throws LifecycleException {  
      
        // Start our subordinate components, if any  
        if ((loader != null) && (loader instanceof Lifecycle))  
            ((Lifecycle) loader).start();  
        logger = null;  
        getLogger();  
        if ((manager != null) && (manager instanceof Lifecycle))  
            ((Lifecycle) manager).start();  
        if ((cluster != null) && (cluster instanceof Lifecycle))  
            ((Lifecycle) cluster).start();  
        Realm realm = getRealmInternal();  
        if ((realm != null) && (realm instanceof Lifecycle))  
            ((Lifecycle) realm).start();  
        if ((resources != null) && (resources instanceof Lifecycle))  
            ((Lifecycle) resources).start();  
      
        // 找出Engine的子容器,也就是Host容器
        Container children[] = findChildren();  
        List<Future<Void>> results = new ArrayList<Future<Void>>();  
        //利用執行緒池呼叫Host的start方法
        for (int i = 0; i < children.length; i++) {  
            results.add(startStopExecutor.submit(new StartChild(children[i])));  
        }  
      
        boolean fail = false;  
        for (Future<Void> result : results) {  
            try {  
                result.get();  
            } catch (Exception e) {  
                log.error(sm.getString("containerBase.threadedStartFailed"), e);  
                fail = true;  
            }  
      
        }  
        if (fail) {  
            throw new LifecycleException(  
                    sm.getString("containerBase.threadedStartFailed"));  
        }  
      
        // Start the Valves in our pipeline (including the basic), if any  
        if (pipeline instanceof Lifecycle)  
            ((Lifecycle) pipeline).start();  
        setState(LifecycleState.STARTING);  
        // Start our thread  
        threadStart();  
      
    } 
    
  • Host元件的start:經過前面介紹,我們知道Host元件的start方法最後還是會呼叫自己startInternal方法;

  • Context元件的start:觸發Wrapper的start,載入filter、Servlet等;

  • Wrapper元件的start:

這邊我們重點看下StandardContext的startInternal,這個方法乾的事情比較多:

 protected synchronized void startInternal() throws LifecycleException {

        if(log.isDebugEnabled())
            log.debug("Starting " + getBaseName());

        // Send j2ee.state.starting notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.starting",
                    this.getObjectName(), sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }

        setConfigured(false);
        boolean ok = true;

        // Currently this is effectively a NO-OP but needs to be called to
        // ensure the NamingResources follows the correct lifecycle
        if (namingResources != null) {
            namingResources.start();
        }

        // Post work directory
        postWorkDirectory();

        // Add missing components as necessary
        if (getResources() == null) {   // (1) Required by Loader
            if (log.isDebugEnabled())
                log.debug("Configuring default Resources");

            try {
                setResources(new StandardRoot(this));
            } catch (IllegalArgumentException e) {
                log.error(sm.getString("standardContext.resourcesInit"), e);
                ok = false;
            }
        }
        if (ok) {
            resourcesStart();
        }

        if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

        // An explicit cookie processor hasn't been specified; use the default
        if (cookieProcessor == null) {
            cookieProcessor = new Rfc6265CookieProcessor();
        }

        // Initialize character set mapper
        getCharsetMapper();

        // Validate required extensions
        boolean dependencyCheck = true;
        try {
            dependencyCheck = ExtensionValidator.validateApplication
                (getResources(), this);
        } catch (IOException ioe) {
            log.error(sm.getString("standardContext.extensionValidationError"), ioe);
            dependencyCheck = false;
        }

        if (!dependencyCheck) {
            // do not make application available if dependency check fails
            ok = false;
        }

        // Reading the "catalina.useNaming" environment variable
        String useNamingProperty = System.getProperty("catalina.useNaming");
        if ((useNamingProperty != null)
            && (useNamingProperty.equals("false"))) {
            useNaming = false;
        }

        if (ok && isUseNaming()) {
            if (getNamingContextListener() == null) {
                NamingContextListener ncl = new NamingContextListener();
                ncl.setName(getNamingContextName());
                ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());
                addLifecycleListener(ncl);
                setNamingContextListener(ncl);
            }
        }

        // Standard container startup
        if (log.isDebugEnabled())
            log.debug("Processing standard container startup");


        // Binding thread
        ClassLoader oldCCL = bindThread();

        try {
            if (ok) {
                // Start our subordinate components, if any
                Loader loader = getLoader();
                if (loader instanceof Lifecycle) {
                    ((Lifecycle) loader).start();
                }

                // since the loader just started, the webapp classloader is now
                // created.
                setClassLoaderProperty("clearReferencesRmiTargets",
                        getClearReferencesRmiTargets());
                setClassLoaderProperty("clearReferencesStopThreads",
                        getClearReferencesStopThreads());
                setClassLoaderProperty("clearReferencesStopTimerThreads",
                        getClearReferencesStopTimerThreads());
                setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
                        getClearReferencesHttpClientKeepAliveThread());
                setClassLoaderProperty("clearReferencesObjectStreamClassCaches",
                        getClearReferencesObjectStreamClassCaches());
                setClassLoaderProperty("skipMemoryLeakChecksOnJvmShutdown",
                        getSkipMemoryLeakChecksOnJvmShutdown());

                // By calling unbindThread and bindThread in a row, we setup the
                // current Thread CCL to be the webapp classloader
                unbindThread(oldCCL);
                oldCCL = bindThread();

                // Initialize logger again. Other components might have used it
                // too early, so it should be reset.
                logger = null;
                getLogger();

                Realm realm = getRealmInternal();
                if(null != realm) {
                    if (realm instanceof Lifecycle) {
                        ((Lifecycle) realm).start();
                    }

                    // Place the CredentialHandler into the ServletContext so
                    // applications can have access to it. Wrap it in a "safe"
                    // handler so application's can't modify it.
                    CredentialHandler safeHandler = new CredentialHandler() {
                        @Override
                        public boolean matches(String inputCredentials, String storedCredentials) {
                            return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials);
                        }

                        @Override
                        public String mutate(String inputCredentials) {
                            return getRealmInternal().getCredentialHandler().mutate(inputCredentials);
                        }
                    };
                    context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler);
                }

                // Notify our interested LifecycleListeners
                fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

                // Start our child containers, if not already started
                for (Container child : findChildren()) {
                    if (!child.getState().isAvailable()) {
                        child.start();
                    }
                }

                // Start the Valves in our pipeline (including the basic),
                // if any
                if (pipeline instanceof Lifecycle) {
                    ((Lifecycle) pipeline).start();
                }

                // Acquire clustered manager
                Manager contextManager = null;
                Manager manager = getManager();
                if (manager == null) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("standardContext.cluster.noManager",
                                Boolean.valueOf((getCluster() != null)),
                                Boolean.valueOf(distributable)));
                    }
                    if ( (getCluster() != null) && distributable) {
                        try {
                            contextManager = getCluster().createManager(getName());
                        } catch (Exception ex) {
                            log.error("standardContext.clusterFail", ex);
                            ok = false;
                        }
                    } else {
                        contextManager = new StandardManager();
                    }
                }

                // Configure default manager if none was specified
                if (contextManager != null) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("standardContext.manager",
                                contextManager.getClass().getName()));
                    }
                    setManager(contextManager);
                }

                if (manager!=null && (getCluster() != null) && distributable) {
                    //let the cluster know that there is a context that is distributable
                    //and that it has its own manager
                    getCluster().registerManager(manager);
                }
            }

            if (!getConfigured()) {
                log.error(sm.getString("standardContext.configurationFail"));
                ok = false;
            }

            // We put the resources into the servlet context
            if (ok)
                getServletContext().setAttribute
                    (Globals.RESOURCES_ATTR, getResources());

            if (ok ) {
                if (getInstanceManager() == null) {
                    javax.naming.Context context = null;
                    if (isUseNaming() && getNamingContextListener() != null) {
                        context = getNamingContextListener().getEnvContext();
                    }
                    Map<String, Map<String, String>> injectionMap = buildInjectionMap(
                            getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources());
                    setInstanceManager(new DefaultInstanceManager(context,
                            injectionMap, this, this.getClass().getClassLoader()));
                }
                getServletContext().setAttribute(
                        InstanceManager.class.getName(), getInstanceManager());
                InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());
            }

            // Create context attributes that will be required
            if (ok) {
                getServletContext().setAttribute(
                        JarScanner.class.getName(), getJarScanner());
            }

            // Set up the context init params
            mergeParameters();

            // Call ServletContainerInitializers
            for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
                initializers.entrySet()) {
                try {
                    entry.getKey().onStartup(entry.getValue(),
                            getServletContext());
                } catch (ServletException e) {
                    log.error(sm.getString("standardContext.sciFail"), e);
                    ok = false;
                    break;
                }
            }

            // Configure and call application event listeners
            if (ok) {
                if (!listenerStart()) {
                    log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }

            // Check constraints for uncovered HTTP methods
            // Needs to be after SCIs and listeners as they may programmatically
            // change constraints
            if (ok) {
                checkConstraintsForUncoveredMethods(findConstraints());
            }

            try {
                // Start manager
                Manager manager = getManager();
                if (manager instanceof Lifecycle) {
                    ((Lifecycle) manager).start();
                }
            } catch(Exception e) {
                log.error(sm.getString("standardContext.managerFail"), e);
                ok = false;
            }

            // Configure and call application filters
            if (ok) {
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }

            // Load and initialize all "load on startup" servlets
            if (ok) {
                if (!loadOnStartup(findChildren())){
                    log.error(sm.getString("standardContext.servletFail"));
                    ok = false;
                }
            }

            // Start ContainerBackgroundProcessor thread
            super.threadStart();
        } finally {
            // Unbinding thread
            unbindThread(oldCCL);
        }

        // Set available status depending upon startup success
        if (ok) {
            if (log.isDebugEnabled())
                log.debug("Starting completed");
        } else {
            log.error(sm.getString("standardContext.startFailed", getName()));
        }

        startTime=System.currentTimeMillis();

        // Send j2ee.state.running notification
        if (ok && (this.getObjectName() != null)) {
            Notification notification =
                new Notification("j2ee.state.running", this.getObjectName(),
                                 sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }

        // The WebResources implementation caches references to JAR files. On
        // some platforms these references may lock the JAR files. Since web
        // application start is likely to have read from lots of JARs, trigger
        // a clean-up now.
        getResources().gc();

        // Reinitializing if something went wrong
        if (!ok) {
            setState(LifecycleState.FAILED);
        } else {
            setState(LifecycleState.STARTING);
        }
    }

上面的程式碼有4處重點:呼叫ServletContainerInitializers、啟用Listener、啟用Filter和啟用startup的Servlet。這個和我們平時對Tomcat啟動流程的認知是一致的。

到這裡整個Container元件(包括Engine、Host、Context和Wrapper元件)的start方法呼叫就結束了。接下來是Connector和Mapper元件的start。

//MapperListenner的startInternal
public void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        Engine engine = service.getContainer();
        if (engine == null) {
            return;
        }

        findDefaultHost();

        addListeners(engine);

        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }
    }

以上方法的主要作用是將Host元件和域名對映起來。

最後看下Connector元件的start:

protected void startInternal() throws LifecycleException {

        // Validate settings before starting
        if (getPortWithOffset() < 0) {
            throw new LifecycleException(sm.getString(
                    "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
        }

        setState(LifecycleState.STARTING);

        try {
            //促發protocolHandler元件的start,最後促發endpoint元件的start
            //觸發endpoint時會建立exceutor執行緒池,預設的話核心執行緒數10,最大執行緒數200
            //建立poller執行緒,最大是2個執行緒,如果你機器cpu的核數小於2的話就建立1個
            //建立accetpor執行緒,預設是1個(可以看看Acceptor這個類的原始碼,瞭解下怎麼接收請求的)
            protocolHandler.start();
        } catch (Exception e) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
        }
    }

通過以上一些列複雜的呼叫過程,最終執行完所有在server.xml裡配置的節點的實現類中initInternal和startInternal方法。上面提到的org.apache.catalina.core.StandardServer、org.apache.catalina.core.StandardService、org.apache.catalina.connector.Connector、org.apache.catalina.core.StandardEngine、org.apache.catalina.core.StandardHost、org.apache.catalina.core.StandardContext等等元件的這兩個方法都會呼叫到。

至此,Tomcat已經能開始響應瀏覽器發過來的請求了。至於具體的Tomcat響應請求流程會在後續部落格中介紹。

5. 總結

看了整個啟動流程,雖然邏輯是比較清楚的,但是流程比較上,所以有必要做下總結:

  • step1:Bootstrap作為整個Tomcat主啟動類,最主要的功能是建立Catalina物件,並呼叫它的load和start方法;

  • step2:Catalina的load方法的作用主要是解析conf/server.xml,生成StandardServer物件(此時生成StandardServer物件中已經包含了各種子元件,比如StandardService、StandardEngine等),再觸發StandardServer的init方法;Catalina的start方法又觸發了StandardServer的start方法;

  • step3:StandardServer的init方法和start方法會依次觸發各個子元件的initInternal和startInternal方法。大致的觸發順序是:

    Engine元件的initInternal(這邊要注意的是Engine元件並沒有觸發它的子元件Host、Context和Wrapper的initInternal)-->Executor元件initInternal(處理請求的工作執行緒池)-->Mapper元件初始化(mapper元件初始化也沒幹什麼重要的操作,也沒觸發其他子元件初始化)-->Connector元件初始化(檢查聯結器的protocolHandler的子元件,並觸發其初始化)-->ProtocolHandler元件初始化(觸發Endpoint元件初始化)​

​ Engine元件的startInternal(主要作用是觸發Host元件的start)-->Host元件的startInternal(主要作用是觸發Context元件的startInternal)-->Contextz元件的startInternal(載入呼叫ServletContainerInitializers、載入Listener、載入filtr和startup的Servlet,並且觸發Wrapper元件的startInternal)-->Wrapper元件的startInternal(載入對映Servlet)-->Mapper元件的startInternal(將域名和Host元件對映起來)-->Connector元件的startInternal(protocolHandler元件的start,最後促發endpoint元件的start)

參考

相關文章