Tomcat 7 啟動分析(三)Digester 的使用

weixin_42073629發表於2020-10-26

前一篇文章裡最後看到 Bootstrap 的 main 方法最後會呼叫 org.apache.catalina.startup.Catalina 物件的 load 和 start 兩個方法,那麼就來看看這兩個方法裡面到底做了些什麼。

load 方法:

  1	    /**
  2	     * Start a new server instance.
  3	     */
  4	    public void load() {
  5
  6	        long t1 = System.nanoTime();
  7
  8	        initDirs();
  9
 10	        // Before digester - it may be needed
 11
 12	        initNaming();
 13
 14	        // Create and execute our Digester
 15	        Digester digester = createStartDigester();
 16
 17	        InputSource inputSource = null;
 18	        InputStream inputStream = null;
 19	        File file = null;
 20	        try {
 21	            file = configFile();
 22	            inputStream = new FileInputStream(file);
 23	            inputSource = new InputSource(file.toURI().toURL().toString());
 24	        } catch (Exception e) {
 25	            if (log.isDebugEnabled()) {
 26	                log.debug(sm.getString("catalina.configFail", file), e);
 27	            }
 28	        }
 29	        if (inputStream == null) {
 30	            try {
 31	                inputStream = getClass().getClassLoader()
 32	                    .getResourceAsStream(getConfigFile());
 33	                inputSource = new InputSource
 34	                    (getClass().getClassLoader()
 35	                     .getResource(getConfigFile()).toString());
 36	            } catch (Exception e) {
 37	                if (log.isDebugEnabled()) {
 38	                    log.debug(sm.getString("catalina.configFail",
 39	                            getConfigFile()), e);
 40	                }
 41	            }
 42	        }
 43
 44	        // This should be included in catalina.jar
 45	        // Alternative: don't bother with xml, just create it manually.
 46	        if( inputStream==null ) {
 47	            try {
 48	                inputStream = getClass().getClassLoader()
 49	                        .getResourceAsStream("server-embed.xml");
 50	                inputSource = new InputSource
 51	                (getClass().getClassLoader()
 52	                        .getResource("server-embed.xml").toString());
 53	            } catch (Exception e) {
 54	                if (log.isDebugEnabled()) {
 55	                    log.debug(sm.getString("catalina.configFail",
 56	                            "server-embed.xml"), e);
 57	                }
 58	            }
 59	        }
 60
 61
 62	        if (inputStream == null || inputSource == null) {
 63	            if  (file == null) {
 64	                log.warn(sm.getString("catalina.configFail",
 65	                        getConfigFile() + "] or [server-embed.xml]"));
 66	            } else {
 67	                log.warn(sm.getString("catalina.configFail",
 68	                        file.getAbsolutePath()));
 69	                if (file.exists() && !file.canRead()) {
 70	                    log.warn("Permissions incorrect, read permission is not allowed on the file.");
 71	                }
 72	            }
 73	            return;
 74	        }
 75
 76	        try {
 77	            inputSource.setByteStream(inputStream);
 78	            digester.push(this);
 79	            digester.parse(inputSource);
 80	        } catch (SAXParseException spe) {
 81	            log.warn("Catalina.start using " + getConfigFile() + ": " +
 82	                    spe.getMessage());
 83	            return;
 84	        } catch (Exception e) {
 85	            log.warn("Catalina.start using " + getConfigFile() + ": " , e);
 86	            return;
 87	        } finally {
 88	            try {
 89	                inputStream.close();
 90	            } catch (IOException e) {
 91	                // Ignore
 92	            }
 93	        }
 94
 95	        getServer().setCatalina(this);
 96
 97	        // Stream redirection
 98	        initStreams();
 99
100	        // Start the new server
101	        try {
102	            getServer().init();
103	        } catch (LifecycleException e) {
104	            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
105	                throw new java.lang.Error(e);
106	            } else {
107	                log.error("Catalina.start", e);
108	            }
109
110	        }
111
112	        long t2 = System.nanoTime();
113	        if(log.isInfoEnabled()) {
114	            log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
115	        }
116
117	    }

這個 117 行的程式碼看起來東西挺多,把註釋、異常丟擲、記錄日誌、流關閉、非空判斷這些放在一邊就會發現實際上真正做事的就這麼幾行程式碼: 

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

做的事情就兩個,一是建立一個 Digester 物件,將當前物件壓入 Digester 裡的物件棧頂,根據 inputSource 裡設定的檔案 xml 路徑及所建立的 Digester 物件所包含的解析規則生成相應物件,並呼叫相應方法將物件之間關聯起來。二是呼叫 Server 介面物件的 init 方法。

這裡碰到了 Digester,有必要介紹一下 Digester 的一些基礎知識。一般來說 Java 裡解析 xml 檔案有兩種方式:一種是 Dom4J 之類將檔案全部讀取到記憶體中,在記憶體裡構造一棵 Dom 樹的方式來解析。一種是 SAX 的讀取檔案流,在流中碰到相應的xml節點觸發相應的節點事件回撥相應方法,基於事件的解析方式,優點是不需要先將檔案全部讀取到記憶體中。

Digester 本身是採用 SAX 的解析方式,在其上提供了一層包裝,對於使用者更簡便友好罷了。最早是在 struts 1 裡面用的,後來獨立出來成為 apache 的 Commons 下面的一個單獨的子專案。Tomcat 裡又把它又封裝了一層,為了描述方便,直接拿 Tomcat 裡的 Digester 建一個單獨的 Digester 的例子來介紹。

1	package org.study.digester;
  2
  3	import java.io.IOException;
  4	import java.io.InputStream;
  5	import java.util.ArrayList;
  6	import java.util.HashMap;
  7	import java.util.List;
  8
  9	import junit.framework.Assert;
 10
 11	import org.apache.tomcat.util.digester.Digester;
 12	import org.xml.sax.InputSource;
 13
 14	public class MyDigester {
 15
 16	    private MyServer myServer;
 17
 18	    public MyServer getMyServer() {
 19	        return myServer;
 20	    }
 21
 22	    public void setMyServer(MyServer myServer) {
 23	        this.myServer = myServer;
 24	    }
 25
 26	    private Digester createStartDigester() {
 27	        // 例項化一個Digester物件
 28	        Digester digester = new Digester();
 29
 30	        // 設定為false表示解析xml時不需要進行DTD的規則校驗
 31	        digester.setValidating(false);
 32
 33	        // 是否進行節點設定規則校驗,如果xml中相應節點沒有設定解析規則會在控制檯顯示提示資訊
 34	        digester.setRulesValidation(true);
 35
 36	        // 將xml節點中的className作為假屬性,不必呼叫預設的setter方法(一般的節點屬性在解析時將會以屬性值作為入參呼叫該節點相應物件的setter方法,而className屬性的作用是提示解析器用該屬性的值來例項化物件)
 37	        HashMap, List> fakeAttributes = new HashMap, List>();
 38	        ArrayList attrs = new ArrayList();
 39	        attrs.add("className");
 40	        fakeAttributes.put(Object.class, attrs);
 41	        digester.setFakeAttributes(fakeAttributes);
 42
 43	        // addObjectCreate方法的意思是碰到xml檔案中的Server節點則建立一個MyStandardServer物件
 44	        digester.addObjectCreate("Server",
 45	                "org.study.digester.MyStandardServer", "className");
 46	        // 根據Server節點中的屬性資訊呼叫相應屬性的setter方法,以上面的xml檔案為例則會呼叫setPort、setShutdown方法,入參分別是8005、SHUTDOWN
 47	        digester.addSetProperties("Server");
 48	        // 將Server節點對應的物件作為入參呼叫棧頂物件的setMyServer方法,這裡的棧頂物件即下面的digester.push方法所設定的當前類的物件this,就是說呼叫MyDigester類的setMyServer方法
 49	        digester.addSetNext("Server", "setMyServer",
 50	                "org.study.digester.MyServer");
 51
 52	        // 碰到xml的Server節點下的Listener節點時取className屬性的值作為例項化類例項化一個物件
 53	        digester.addObjectCreate("Server/Listener", null, "className");
 54	        digester.addSetProperties("Server/Listener");
 55	        digester.addSetNext("Server/Listener", "addLifecycleListener",
 56	                "org.apache.catalina.LifecycleListener");
 57
 58	        digester.addObjectCreate("Server/Service",
 59	                "org.study.digester.MyStandardService", "className");
 60	        digester.addSetProperties("Server/Service");
 61	        digester.addSetNext("Server/Service", "addMyService",
 62	                "org.study.digester.MyService");
 63
 64	        digester.addObjectCreate("Server/Service/Listener", null, "className");
 65	        digester.addSetProperties("Server/Service/Listener");
 66	        digester.addSetNext("Server/Service/Listener", "addLifecycleListener",
 67	                "org.apache.catalina.LifecycleListener");
 68	        return digester;
 69	    }
 70
 71	    public MyDigester() {
 72	        Digester digester = createStartDigester();
 73
 74	        InputSource inputSource = null;
 75	        InputStream inputStream = null;
 76	        try {
 77	            String configFile = "myServer.xml";
 78	            inputStream = getClass().getClassLoader().getResourceAsStream(
 79	                    configFile);
 80	            inputSource = new InputSource(getClass().getClassLoader()
 81	                    .getResource(configFile).toString());
 82
 83	            inputSource.setByteStream(inputStream);
 84	            digester.push(this);
 85	            digester.parse(inputSource);
 86	        } catch (Exception e) {
 87	            e.printStackTrace();
 88	        } finally {
 89	            try {
 90	                inputStream.close();
 91	            } catch (IOException e) {
 92	                // Ignore
 93	            }
 94	        }
 95
 96	        getMyServer().setMyDigester(this);
 97	    }
 98
 99	    public static void main(String[] agrs) {
100	        MyDigester md = new MyDigester();
101	        Assert.assertNotNull(md.getMyServer());
102	    }
103	}

上面是我自己寫的一個拿 Tomcat 裡的 Digester 的工具類解析 xml 檔案的例子,關鍵方法的呼叫含義已經在註釋中寫明,解析的是專案原始檔釋出的根目錄下的 myServer.xml 檔案。 

<?xml version='1.0' encoding='utf-8'?>

<Server port="8005" shutdown="SHUTDOWN">

	<Listener
		className="org.apache.catalina.core.JasperListener" />

	<Listener
		className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

	<Service name="Catalina">

		<Listener
			className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
	</Service>
</Server>

Digester 的使用一般來說有4步:

  1. 例項化一個 Digester 物件,並在物件裡設定相應的節點解析規則。
  2. 設定要解析的檔案作為輸入源( InputSource ),這裡 InputSource 與 SAX 裡的一樣,用以表示一個 xml 實體。
  3. 將當前物件壓入棧。
  4. 呼叫 Digester 的 parse 方法解析 xml,生成相應的物件。

第 3 步中將當前物件壓入棧中的作用是可以儲存一個到 Digester 生成的一系列物件直接的引用,方便後續使用而已,所以不必是當前物件,只要有一個地方存放這個引用即可。

這裡有必要說明的是很多文章裡按照程式碼順序來描述 Digester 的所謂棧模型來講述 addSetNext 方法時的呼叫物件,實際上我的理解 addSetNext 方法具體哪個呼叫物件與XML檔案裡的節點樹形結構相關,當前節點的父節點是哪個物件該物件就是呼叫物件。可以試驗一下把這裡的程式碼順序打亂仍然可以按規則解析出來,createStartDigester 方法只是在定義解析規則,具體解析與 addObjectCreate、addSetProperties、addSetNext 這些方法的呼叫順序無關。Digester 自己內部在解析 xml 的節點元素時增加了一個 rule 的概念,addObjectCreate、addSetProperties、addSetNext 這些方法內部實際上是在新增 rule,在最後解析 xml 時將會根據讀取到的節點匹配相應節點路徑下的 rule,呼叫內部的方法。關於 Tomcat 內的 Digester 的解析原理以後可以單獨寫篇文章分析一下。

該示例的程式碼在附件中,將其放入 tomcat 7 的原始碼環境中即可直接執行。

看懂了上面的例子 Catalina 的 createStartDigester 方法應該就可以看懂了,它只是比示例要處理的節點型別更多,並且增加幾個自定義的解析規則,如 384 行在碰到 Server/GlobalNamingResources/ 節點時將會呼叫 org.apache.catalina.startup.NamingRuleSet 類中的 addRuleInstances 方法新增解析規則。

要解析的 XML 檔案預設會先找 conf/server.xml,如果當前專案找不到則通過其他路徑找 xml 檔案來解析,這裡以預設情況為例將會解析 server.xml

<?xml version='1.0' encoding='utf-8'?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- Note:  A "Server" is not itself a "Container", so you may not
     define subcomponents such as "Valves" at this level.
     Documentation at /docs/config/server.html
 -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!--APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->
  <Listener className="org.apache.catalina.core.JasperListener" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- A "Service" is a collection of one or more "Connectors" that share
       a single "Container" Note:  A "Service" is not itself a "Container",
       so you may not define subcomponents such as "Valves" at this level.
       Documentation at /docs/config/service.html
   -->
  <Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL HTTP/1.1 Connector on port 8080
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- A "Connector" using the shared thread pool-->
    <!--
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    -->
    <!-- Define a SSL HTTP/1.1 Connector on port 8443
         This connector uses the JSSE configuration, when using APR, the
         connector should be using the OpenSSL style configuration
         described in the APR documentation -->
    <!--
    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" />
    -->

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />


    <!-- An Engine represents the entry point (within Catalina) that processes
         every request.  The Engine implementation for Tomcat stand alone
         analyzes the HTTP headers included with the request, and passes them
         on to the appropriate Host (virtual host).
         Documentation at /docs/config/engine.html -->

    <!-- You should set jvmRoute to support load-balancing via AJP ie :
    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
    -->
    <Engine name="Catalina" defaultHost="localhost">

      <!--For clustering, please take a look at documentation at:
          /docs/cluster-howto.html  (simple how to)
          /docs/config/cluster.html (reference documentation) -->
      <!--
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
      -->

      <!-- Use the LockOutRealm to prevent attempts to guess user passwords
           via a brute-force attack -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- This Realm uses the UserDatabase configured in the global JNDI
             resources under the key "UserDatabase".  Any edits
             that are performed against this UserDatabase are immediately
             available for use by the Realm.  -->
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

這樣經過對 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 等等一系列物件,這些物件從前到後前一個包含後一個物件的引用(一對一或一對多的關係)。

解析完 xml 之後關閉檔案流,接著設定 StandardServer 物件(該物件在上面解析 xml 時)的 catalina 的引用為當前物件,這種物件間的雙向引用在 Tomcat 的很多地方都會碰到。

接下來將呼叫 StandardServer 物件的 init 方法。

上面分析的是 Catalina 的 load 方法,上一篇文章裡看到 Bootstrap 類啟動時還會呼叫 Catalina 物件的 start 方法,程式碼如下:

 1	    /**
 2	     * Start a new server instance.
 3	     */
 4	    public void start() {
 5
 6	        if (getServer() == null) {
 7	            load();
 8	        }
 9
10	        if (getServer() == null) {
11	            log.fatal("Cannot start server. Server instance is not configured.");
12	            return;
13	        }
14
15	        long t1 = System.nanoTime();
16
17	        // Start the new server
18	        try {
19	            getServer().start();
20	        } catch (LifecycleException e) {
21	            log.fatal(sm.getString("catalina.serverStartFail"), e);
22	            try {
23	                getServer().destroy();
24	            } catch (LifecycleException e1) {
25	                log.debug("destroy() failed for failed Server ", e1);
26	            }
27	            return;
28	        }
29
30	        long t2 = System.nanoTime();
31	        if(log.isInfoEnabled()) {
32	            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
33	        }
34
35	        // Register shutdown hook
36	        if (useShutdownHook) {
37	            if (shutdownHook == null) {
38	                shutdownHook = new CatalinaShutdownHook();
39	            }
40	            Runtime.getRuntime().addShutdownHook(shutdownHook);
41
42	            // If JULI is being used, disable JULI's shutdown hook since
43	            // shutdown hooks run in parallel and log messages may be lost
44	            // if JULI's hook completes before the CatalinaShutdownHook()
45	            LogManager logManager = LogManager.getLogManager();
46	            if (logManager instanceof ClassLoaderLogManager) {
47	                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
48	                        false);
49	            }
50	        }
51
52	        if (await) {
53	            await();
54	            stop();
55	        }
56	    }

這裡最主要的是呼叫 StandardServer 物件的 start 方法。

經過以上分析發現,在解析 xml 產生相應一系列物件後會順序呼叫 StandardServer 物件的 init、start 方法,這裡將會涉及到 Tomcat 的容器生命週期( Lifecycle ),關於這點留到下一篇文章中分析。

相關文章