Tomcat8.5.43源碼分析-(4)Tomcat啓動過程探究 第二部分 Web應用加載

我們回到Tomcat啓動過程探究 第一部分的結尾,回到StandardService的初始化方法initInternal()。

StandardService的重點方法有四個:

  • engine.init():初始化Servlet引擎,引擎只負責請求的處理不需要考慮請求連接,協議等等。一個Service存在一個對應的Engine。
  • executor.init():初始化線程池,該線程池可以在組件中共享。默認實現爲StandardThreadExecutor。
  • mapperListener.init():初始化監聽路由。MapperListener未重寫生命週期中的init()方法,將在start()方法中詳細討論。
  • connector.init():初始化連接器,connector負責監聽、讀取請求,對請求進行制定協議的解析,匹配正確的處理容器,反饋響應。Server.xml中的默認的兩個Connector如下,關於協議這塊此處不再展開,以後若有機會給予補充:
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" /><!HTTP 1.1協議>
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /><!AJP 1.3協議>

在StandardServer.init()方法中,可以看到Server和Service是一對多的關係。爲此,乾脆先整理各層級容器直接的關係圖:

接下來,我們關注Engine(StandardEngine)的初始化時會做些什麼。

首先在StandardEngine構造方法裏,我發現了有意思的東西:

    /**
     * Create a new StandardEngine component with the default basic Valve.
     */
    public StandardEngine() {

        super();
        pipeline.setBasic(new StandardEngineValve());
        /* Set the jmvRoute using the system property jvmRoute */
        try {
            setJvmRoute(System.getProperty("jvmRoute"));
        } catch(Exception ex) {
            log.warn(sm.getString("standardEngine.jvmRouteFail"));
        }
        // By default, the engine will hold the reloading thread
        backgroundProcessorDelay = 10;

    }

注意這一句:

pipeline.setBasic(new StandardEngineValve());

查詢資料後,知道:

Pipeline(管道)採用了責任鏈的方式處理客戶端的請求,每一個Valve(閥)表示責任鏈上的每一個處理器。

StandardPipeline重寫了startInternal()方法,Pieline和Valve也將在start()方法中詳細討論。

至此,從Bootstrap.load()開始的長途跋涉就告一段落了。不過後面是流程更復雜的Bootstrap.start()。

經過了load的查看源碼的經歷,很容易就找到了start的關鍵路徑爲:Bootstrap.start()->Catalina.start()->StandardServer.startInternal()

    /**
     * Start nested components ({@link Service}s) and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @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();
            }
        }
    }

StandardServer此刻註冊的監聽器列表如下:

遍歷啓動StandardService.startInternal():

    /**
     * Start nested components ({@link Executor}s, {@link Connector}s and
     * {@link Container}s) and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    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) {
                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);
                }
            }
        }
    }

此刻StandardService的監聽器列表爲空。

在Catalina.createStartDigester()中,給StandardEngine通過addChild添加了一個child,即StandardHost。

在容器抽象類ContainerBase中startInternal()方法有這樣一段:

        // Start our child containers, if any
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<>();
        for (int i = 0; i < children.length; i++) {
            results.add(startStopExecutor.submit(new StartChild(children[i])));
        }

它代表了當一個父容器start的時候,子容器會開啓新線程。並執行子容器的start()方法。

接下來我們關注StandardHost的startInternal()方法:

    /**
     * Start this component and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Set error report valve
        String errorValve = getErrorReportValveClass();
        if ((errorValve != null) && (!errorValve.equals(""))) {
            try {
                boolean found = false;
                Valve[] valves = getPipeline().getValves();
                for (Valve valve : valves) {
                    if (errorValve.equals(valve.getClass().getName())) {
                        found = true;
                        break;
                    }
                }
                if(!found) {
                    Valve valve =
                        (Valve) Class.forName(errorValve).getConstructor().newInstance();
                    getPipeline().addValve(valve);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString(
                        "standardHost.invalidErrorReportValveClass",
                        errorValve), t);
            }
        }
        super.startInternal();
    }

這個方法只設置了一個關閉閥門,並沒有做太多的事情。

但此刻StandardHost的監聽器列表:

我們知道接下來HostConfig會接收到Lifecycle.START_EVENT的消息。

我們看下HostConfig在接收到消息後會做什麼:

    /**
     * Process the START event for an associated Host.
     *
     * @param event The lifecycle event that has occurred
     */
    @Override
    public void lifecycleEvent(LifecycleEvent event) {

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
                setContextClass(((StandardHost) host).getContextClass());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            check();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }

首先是標識符的設置,這邊只說比較重要的標識符:

  • CopyXML:若爲true,複製context.xml到$CATALINA_BASE/conf/<Engine名稱>/<Host名稱>目錄下
  • DeployXML:若爲true,Digester解析context.xml並創建Context對象。

之後就是HostConfig.start()方法了:

    /**
     * Process a "start" event for this Host.
     */
    public void start() {

        if (log.isDebugEnabled())
            log.debug(sm.getString("hostConfig.start"));

        try {
            ObjectName hostON = host.getObjectName();
            oname = new ObjectName
                (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());
        } catch (Exception e) {
            log.warn(sm.getString("hostConfig.jmx.register", oname), e);
        }

        if (!host.getAppBaseFile().isDirectory()) {
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }

        if (host.getDeployOnStartup())
            deployApps();

    }

這裏比較有意思的一段是:

            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());

將當前監聽器註冊到MBean,由其管理。關於MBean,後面會另做一章介紹,這裏只要知道是註冊上去就可以了。

HostConfig.delpoyApps()方法:

    /**
     * Deploy applications for any directories or WAR files that are found
     * in our "application root" directory.
     */
    protected void deployApps() {

        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);

    }
  • deployDescriptors:從Context描述文件部署context。例如:$CATALINA_BASE/conf/Catalina/localhost下創建app.xml文件。
  • deployWARs:從War包部署context。例如:把Web工程的打包的War包複製到Host指定的appBase下。
  • deployDirectories:從Web路徑部署context。例如:把Web工程的所有資源文件複製到Host指定的appBase下。

這三個方法,其實是在不同的地方做同樣一件事情,以deployDirectories爲例:

  1. 創建新的線程,通過方法對應的方式,解析生成Context實例StandardContext。
  2. 給StandardContext設置監聽器ContextConfig。deployDirectory(ContextName cn, File dir):
                Class<?> clazz = Class.forName(host.getConfigClass());
                LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();

    這裏的關係實際上已在Catalina.createStartDigester()中配置。

  3. 將StandardContext設置爲StandardHost的child,若Host已經啓動,則會直接啓動context:
host.addChild(context);

 接下來執行的是StandardContext.startInternal(),這個方法非常的冗長,這裏只選取重要的部分:

        if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }
                Loader loader = getLoader();
                if (loader instanceof Lifecycle) {
                    ((Lifecycle) loader).start();
                }

上兩段爲創建和啓動Web應用類加載器WebappClassLoader。

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

 CONFIGURE_START_EVENT事件的,通知。順便整理髮送該通知時,StandardContext的監聽列表:

 ContextConfig接收到CONFIGURE_START_EVENT後,會執行configureStart()->webConfig()。

webConfig()是真正解析web.xml的方法,是和Web開發關係最緊密,也是我們最常見的配置xml。我們重點看一下這個方法,已經在重要的步驟上添加了註釋:

    /**
     * Scan the web.xml files that apply to the web application and merge them
     * using the rules defined in the spec. For the global web.xml files,
     * where there is duplicate configuration, the most specific level wins. ie
     * an application's web.xml takes precedence over the host level or global
     * web.xml file.
     */
    protected void webConfig() {
        /*
         * Anything and everything can override the global and host defaults.
         * This is implemented in two parts
         * - Handle as a web fragment that gets added after everything else so
         *   everything else takes priority
         * - Mark Servlets as overridable so SCI configuration can replace
         *   configuration from the defaults
         */

        /*
         * The rules for annotation scanning are not as clear-cut as one might
         * think. Tomcat implements the following process:
         * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
         *   which Servlet spec version is declared in web.xml. The EG has
         *   confirmed this is the expected behaviour.
         * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
         *   web.xml is marked as metadata-complete, JARs are still processed
         *   for SCIs.
         * - If metadata-complete=true and an absolute ordering is specified,
         *   JARs excluded from the ordering are also excluded from the SCI
         *   processing.
         * - If an SCI has a @HandlesType annotation then all classes (except
         *   those in JARs excluded from an absolute ordering) need to be
         *   scanned to check if they match.
         */
        WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
                context.getXmlValidation(), context.getXmlBlockExternal());

        Set<WebXml> defaults = new HashSet<>();
        defaults.add(getDefaultWebXmlFragment(webXmlParser));

        WebXml webXml = createWebXml();

        // Parse context level web.xml
        InputSource contextWebXml = getContextWebXmlSource();//解析容器基本的web.xml
        if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
            ok = false;
        }

        ServletContext sContext = context.getServletContext();

        // Ordering is important here

        // Step 1. Identify all the JARs packaged with the application and those
        // provided by the container. If any of the application JARs have a
        // web-fragment.xml it will be parsed at this point. web-fragment.xml
        // files are ignored for container provided JARs.
        //掃描/WEB-INF/lib下的所有JAR包,如果包含/META-INF/web-fragment.xml這類片段webXml
        //則創建片段webXml
        Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

        // Step 2. Order the fragments.
        //對片段webXml進行排序,排序結果影響了Filter的執行順序
        Set<WebXml> orderedFragments = null;
        orderedFragments =
                WebXml.orderWebFragments(webXml, fragments, sContext);

        // Step 3. Look for ServletContainerInitializer implementations
        //查找並創建ServletContainerInitializer的實現類
        if (ok) {
            processServletContainerInitializers();
        }

        if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
            // Steps 4 & 5.
            //註解配置解析,將解析結果合併到web.xml
            processClasses(webXml, orderedFragments);
        }

        if (!webXml.isMetadataComplete()) {
            // Step 6. Merge web-fragment.xml files into the main web.xml
            // file.
            //合併片段webXml按照順序到web.xml
            if (ok) {
                ok = webXml.merge(orderedFragments);
            }

            // Step 7. Apply global defaults
            // Have to merge defaults before JSP conversion since defaults
            // provide JSP servlet definition.
            //配置JSP Servlet
            webXml.merge(defaults);

            // Step 8. Convert explicitly mentioned jsps to servlets
            if (ok) {
                convertJsps(webXml);
            }

            // Step 9. Apply merged web.xml to Context
            //使用合併後的web.xml配置當前StandardContext,包括Servlet、Filter、Listener
            //並針對Servlet創建StandardWrapper,添加到StandardContext
            if (ok) {
                configureContext(webXml);
            }
        } else {
            webXml.merge(defaults);
            convertJsps(webXml);
            configureContext(webXml);
        }

        if (context.getLogEffectiveWebXml()) {
            log.info("web.xml:\n" + webXml.toXml());
        }

        // Always need to look for static resources
        // Step 10. Look for static resources packaged in JARs
        //添加META-INF/resources/下的靜態資源到standardContext
        if (ok) {
            // Spec does not define an order.
            // Use ordered JARs followed by remaining JARs
            Set<WebXml> resourceJars = new LinkedHashSet<>();
            for (WebXml fragment : orderedFragments) {
                resourceJars.add(fragment);
            }
            for (WebXml fragment : fragments.values()) {
                if (!resourceJars.contains(fragment)) {
                    resourceJars.add(fragment);
                }
            }
            processResourceJARs(resourceJars);
            // See also StandardContext.resourcesStart() for
            // WEB-INF/classes/META-INF/resources configuration
        }

        // Step 11. Apply the ServletContainerInitializer config to the
        // context
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry :
                        initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(
                            entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(
                            entry.getKey(), entry.getValue());
                }
            }
        }
    }

至此,終於真正的完成了web.xml的合併、解析,並應用於StandardContext。

我們回到StandardContext.startInternal(),看看接下來還做了什麼:

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

啓動StandardWrapper後,StandarWrapper.startInternal()只有一個作用,即由MBean發送Notification廣播。

隨後沿着addChild這條線,會繼續調用StandarWrapper.load()->loadServlet()方法,開始初始化和創建Servlet實例。

至此, Web應用的加載過程就完畢了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章