曹工說Tomcat3:深入理解 Tomcat Digester

三國夢迴發表於2019-07-03

一、前言

我寫部落格主要靠自己實戰,理論知識不是很強,要全面介紹Tomcat Digester,還是需要一定的理論功底。翻閱了一些介紹 Digester 的書籍、部落格,發現不是很系統,最後發現還是官方文件最全面。這裡我就把其全文翻譯一遍吧,部分不好懂的地方會做些補充。

前面寫了兩篇 ,一篇是 sax 模型的,一篇是模仿著 Tomcat 的Digester 寫的。大家可以先看看這兩篇,而且很有必要照著文中的原始碼跑一下,原始碼都放在基友網站了。

 

官方文件在:http://commons.apache.org/proper/commons-digester/guide/core.html 

因為我是從Tomcat 瞭解到Digester,寫完之前都沒有意識到 Digester早已是一個獨立的 project,所以下面整體都是依照 Tomcat 裡面的 org.apache.tomcat.util.digester 包的 packageSummary.html 來譯的。

原文在:https://tomcat.apache.org/tomcat-7.0-doc/api/index.html  的 org.apache.tomcat.util.digester 包的packageSummary。

 

 

二、譯文

1、介紹

在很多需要處理xml格式的程式環境中,用事件驅動的方式去處理 xml 文件是相當有用的。在事件驅動模型下,通俗點說就是,遇到特定的xml元素時,建立特定的 Java 物件,或者呼叫物件的方法。熟悉 SAX 模型的開發者能意識到,Digester 提供了更高階別的抽象,提供了對 SAX 事件進行處置的,對開發者更友好的介面,因為對 xml 文件進行遍歷的細節都被隱藏起來了,讓開發者能夠專心編寫 xml 元素的處理規則。

 

為了使用 Digester,需要進行以下幾步:

1、建立一個org.apache.commons.digester.Digester 類的物件。之前建立的物件可以安全複用,只要之前的任何操作都已經完成。同時,注意不要在多個執行緒裡操作同一個Digester 物件,因為其是執行緒不安全的。

2、設定該物件的屬性,這些屬性會影響解析過程。(譯者注:比如是否驗證xml、是否使用執行緒上下文載入器等)

3、(可選)往 Digester 的棧中,壓入初始物件。(注:初始物件的主要作用是接收解析 xml 後的根物件。比如,Tomcat 解析Server.xml後,會生成一個 StandardServer 根物件,為了獲得該物件的引用,在原始碼中,初始壓入了 catalina 類物件作為初始物件,最終呼叫 catalina 的 setServer 方法來將 StandardServer 根物件設定進去;另外一處原始碼中,往初始棧壓入了 ArrayList 物件,然後呼叫 ArrayList 的 add 方法來接收解析出來的物件)

4、註冊 xml 元素匹配模式,及對應的處理規則。你可以針對一個 xml 元素匹配模式,指定任意多個規則,這些規則會用 list 儲存,應用規則時,會遍歷 list 。

5、呼叫 digester 物件的 parse()方法,傳入一個 xml 文件的引用。這個 xml 文件可以用多種方式傳入,比如 InputStream,或者File等。注意的是,需要準備好捕獲該方法丟擲的IOException、SAXException,以及自定義規則中可能丟擲的執行時異常。(注:比如處理到我們想要的元素後,想立即中斷後續處理,可手動丟擲異常,這時候就需要在外層捕獲)

 

2、樣例程式碼

注:筆者也寫過Digester的例項程式碼,路徑:https://github.com/cctvckl/tomcat-saxtest/blob/master/src/main/java/com/coder/DigesterTest.java

以下官方文件中的示例,筆者也已經上傳到了 https://github.com/cctvckl/tomcat-saxtest/tree/master/src/main/java/mypackage,只要執行Test類即可看到效果。

2.1 解析簡單物件樹

假設我們現在有兩個簡單的java bean,Foo and Bar:

package mypackage;
  public class Foo {
    public void addBar(Bar bar);
    public Bar findBar(int id);
    public Iterator getBars();
    public String getName();
    public void setName(String name);
  }

  public mypackage;
  public class Bar {
    public int getId();
    public void setId(int id);
    public String getTitle();
    public void setTitle(String title);
  }

 

假設現在你希望使用 Digester 來解析下面的xml 文件:

<foo name="The Parent">
    <bar id="123" title="The First Child"/>
    <bar id="456" title="The Second Child"/>
  </foo>

那麼,一個簡單的方式就是像下面這樣,利用Digester 去設定解析規則,然後去處理該xml文件即可:

1   Digester digester = new Digester();
2   digester.setValidating(false);
3   digester.addObjectCreate("foo", "mypackage.Foo");
4   digester.addSetProperties("foo");
5   digester.addObjectCreate("foo/bar", "mypackage.Bar");
6   digester.addSetProperties("foo/bar");
7   digester.addSetNext("foo/bar", "addBar", "mypackage.Bar");
8   Foo foo = (Foo) digester.parse();

 

按照時間順序,這些規則將會像下面這樣一一生效:

1、當遇到最外層的<foo> 元素時,建立一個 mypackage.foo 類的物件,並壓入物件棧。在遇到</foo>時,該物件將被彈出。

2、基於xml元素的屬性,來設定棧頂物件的屬性。(比如此時棧頂物件為foo)

3、當遇到內嵌的<bar>元素時,建立一個 mypackage.bar類的物件,壓入物件棧。

4、基於xml元素的屬性,來設定棧頂物件的屬性。(此時棧頂為bar)

5、setNext方法,一共三個引數,表示:遇到foo/bar 元素時,此時棧頂為bar,棧頂的下一個元素為foo,對棧頂物件的前一個物件foo呼叫 addBar 方法,方法的引數型別為 mypackage.Bar,傳入的引數為棧頂物件。

注:規則5不好理解,大家參考以下實現程式碼就理解了:

 1     // org.apache.tomcat.util.digester.SetNextRule#end
 2     public void end(String namespace, String name) throws Exception {
 3 
 4         // Identify the objects to be used
 5         Object child = digester.peek(0);
 6         Object parent = digester.peek(1);
 7 
 8         // Call the specified method
 9         IntrospectionUtils.callMethod1(parent, methodName,
10                 child, paramType, digester.getClassLoader());
11                 
12     }

 

一旦解析完成,首個被壓入棧內的物件將被返回。此時,該物件的所有屬性及子元素都已被設定,程式可以拿來用了。

 

2.2  digester 處理 struts 配置檔案

這裡說說 digester 的歷史。Digester 包之所以被建立,是因為 Struts 1 中的 Controller 需要一個魯棒的、靈活的、簡單的方式來解析 struts-config.xml。該配置檔案幾乎包含了基於Struts的程式的方方面面(注:大家可以想象,當時註解根本不流行,我剛下載了 Struts 2的程式碼,沒找到利用 Digester 的程式碼,又下載了 Struts1 的原始碼,在Struts 1的原始碼裡才找到,Struts 1,我13年本科畢業,根本沒用過這玩意,學校裡學的都是 Struts 2了,可以想象這個多古老)。但也正因如此,Struts 1 的Controller 包含了這樣一個在真實專案中廣泛應用的,利用Digester來解析xml 的例子。

注:這裡摘錄了 org.apache.struts.action.ActionServlet 類中配置和使用 Digester 的例子。

 1     protected void initServlet()
 3         // Remember our servlet name
 4         this.servletName = getServletConfig().getServletName();
 5 
 6         // Prepare a Digester to scan the web application deployment descriptor
 7         Digester digester = new Digester();
 8 
 9         digester.push(this);
10         digester.setNamespaceAware(true);
11         digester.setValidating(false);
12 
13         // Register our local copy of the DTDs that we can find
14         for (int i = 0; i < registrations.length; i += 2) {
15             URL url = this.getClass().getResource(registrations[i + 1]);
16 
17             if (url != null) {
18                 digester.register(registrations[i], url.toString());
19             }
20         }
21 
22         // Configure the processing rules that we need
23         digester.addCallMethod("web-app/servlet-mapping", "addServletMapping", 2);
24         digester.addCallParam("web-app/servlet-mapping/servlet-name", 0);
25         digester.addCallParam("web-app/servlet-mapping/url-pattern", 1);31 
32         InputStream input =
33             getServletContext().getResourceAsStream("/WEB-INF/web.xml");39 
41         digester.parse(input);56 }

 

2.3 解析 xml 元素的body context

Digester 也可以用來解析xml 元素的 body text 。下面的例子,就以解析 WEB-INF/web.xml 為例。

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

<web-app>
    <servlet>
        <servlet-name>action</servlet-name>
        <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
        <init-param>
            <param-name>application</param-name>
            <param-value>org.apache.struts.example.ApplicationResources</param-value>
        </init-param>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
    </servlet>
</web-app>

 

假設我們的 Servlet class 如下:

 1 package mypackage;
 2 
 3 import lombok.Data;
 4 
 5 import java.util.ArrayList;
 6 import java.util.List;
 7 
 8 @Data
 9 public class ServletBean {
10     private String servletName;
11     private String servletClass;
12 
13     private List<InitParam> initParams = new ArrayList<>();
14 
15     public void addInitParam(String name, String value){
16         initParams.add(new InitParam(name,value));
17     }
18 
19 }

 

 1 package mypackage;
 2 
 3 import lombok.AllArgsConstructor;
 4 import lombok.Data;
 5 
 6 
 7 @Data
 8 @AllArgsConstructor
 9 public class InitParam {
10     private String name;
11 
12     private String value;
13 
14 
15 }

 

解析程式碼如下所示:

 1 package mypackage;
 2 
 3 import org.apache.commons.digester3.Digester;
 4 import org.xml.sax.SAXException;
 5 
 6 import java.io.IOException;
 7 import java.io.InputStream;
 8 

16 public class WebXmlParseTest {
17     public static void main(String[] args) {
18         Digester digester = new Digester();
19         digester.setValidating(false);
20 
21         digester.addObjectCreate("web-app/servlet",
22                 "mypackage.ServletBean");
23         digester.addCallMethod("web-app/servlet/servlet-name", "setServletName", 0);
24         digester.addCallMethod("web-app/servlet/servlet-class",
25                 "setServletClass", 0);
26         digester.addCallMethod("web-app/servlet/init-param",
27                 "addInitParam", 2);
28         digester.addCallParam("web-app/servlet/init-param/param-name", 0);
29         digester.addCallParam("web-app/servlet/init-param/param-value", 1);
30 
31         InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("web.xml");
32         try {
33             ServletBean servletBean = (ServletBean) digester.parse(inputStream);
34             System.out.println(servletBean);
35         } catch (IOException | SAXException e) {
36             e.printStackTrace();
37         }
38     }
39 }

 

執行效果如下:

 

注:說實話,這個真的相當方便,很多rule都幫我們定義好了。簡直驚豔!

 

3、Digester 配置

以下屬性均需要在呼叫parse()之前呼叫,否則只能下次呼叫時才生效。

 

屬性 描述
classLoader 指定解析規則時,遇到需要載入class時,要使用的classloader(比如 ObjectCreateRule 規則)。如果未指定,預設使用執行緒上下文載入器(useContextClassLoader 為 true)時,否則使用Digester類的類載入器
errorHandler 可選,指定ErrorHandler,當解析異常發生時被呼叫。預設的異常解析器只會記錄日誌,但是Digester依然會繼續解析
namespaceAware 不甚理解,請參考官方文件,
ruleNamespaceURi 不甚理解,請參考官方文件
validating 驗證xml文件的dtd規則
useContextClassLoader 是否使用執行緒上下文載入器去載入class,當classLoader被設定時,該屬性被忽略

 

 

 

 

 

 

 

 

注:關於namespace、dtd這塊,我本身水平有限,還需學習研究。請大家參考相關部落格及官方文件。

 

 

 

 

4、物件棧

Digester一個廣泛的應用是用來基於xml文件,構建 Java 物件的樹形結構。事實上,Digester包被建立時,就是Struts為了基於struts-config.xml來配置Struts 的Controller而誕生的(一開始,Digester包在Struts中,後來移到了 Commons 專案,因為大家覺得這個技術足夠通用)。

為了方便使用,Digester 暴露了內部棧的相關方法,這些方法可以在rule 中被使用(digester 預定義的或者我們自己定義的)。棧的相關方法如下:

clear 清空棧內元素
peek 獲取棧頂元素,但不移除
pop   移除棧頂元素並返回該元素
push   將元素壓入棧內

 

 

 

 

一個典型的模式就是,首先觸發一條規則,在遇到元素的開始標記時,建立一個新的物件。該物件將一直待在棧內,直到該物件的所有巢狀元素及content都已被處理。當遇到結束標記時,將元素彈出棧。如你前面看到的,

 規則即可滿足這個功能。

該模式的問題是:

1、我怎麼講物件關聯起來? Digester支援以下規則:在棧頂物件的下一個物件上,呼叫rule指定的方法,方法引數為棧頂物件(即前文程式碼中的setNext規則)。

2、我怎麼獲取第一個物件的引用?因為xml文件一般是樹形結構,最早壓入的會作為根節點,體現在java 物件時,也會由第一個物件來持有其內嵌的其他物件。所以,我們需要一種方式來獲取這個根物件。在 object create 規則裡,首個壓入的物件,會在遇到其結束標記時被彈出,但是 Digester會幫我們維護首個被壓入棧內的物件的引用,並被返回給 parse() 方法。 或者還有另一種方法,在呼叫parse 方法前,手動壓入一個物件,並利用setNext規則建立該物件和 xml 文件中根物件之間的父子關係。

 

5、元素匹配模式

Digester的一個重要特性,就是其可以根據你指定的匹配模式,自動導航到對應的xml元素,完全不需要開發者操心。換言之,開發者只需要關注在xml中遇到特定模式的xml元素時,需要進行什麼操作就行了。一個很簡單的元素匹配模式的例子是僅指定一個簡單字串,比如“a”,該模式將在解析時,每次遇到一個頂層的<a>標籤時被匹配。值得注意的是,內嵌的<a>元素,並不能匹配該模式。另一個稍微複雜的例子是“a/b”,該模式將在匹配到一個頂級<a>元素內巢狀的<b>元素時被匹配。同樣,文件內出現多少次,該模式就被匹配多少次。

 

我們以例子說話:

 1  <a>         -- Matches pattern "a"
 2     <b>       -- Matches pattern "a/b"
 3       <c/>    -- Matches pattern "a/b/c"
 4       <c/>    -- Matches pattern "a/b/c"
 5     </b>
 6     <b>       -- Matches pattern "a/b"
 7       <c/>    -- Matches pattern "a/b/c"
 8       <c/>    -- Matches pattern "a/b/c"
 9       <c/>    -- Matches pattern "a/b/c"
10     </b>
11   </a>

 

當然,我們也可以匹配某一個特定的元素,而不管它被巢狀在哪一層,要達到這個目的,只需要使用 “*” 即可。比如,“*/a”可以匹配任意的<a>標籤,而不論其巢狀層次如何。當然,很有可能的是,當解析一個xml文件時,我們給一個模式註冊了多個規則。當這種情況發生時,多個規則都能得到匹配(注:就像前面我們的程式碼裡示例的一樣),此時,在觸發 rule 的 begin 和 body 方法時(在解析到xml開始標記和元素內容時觸發),相應的解析規則會按照順序觸發;但是,在解析到xml的結束標記時,觸發 rule 的end方法時,會按照相反的順序觸發。

注:以下即為Digester的endElement方法,在xml解析到元素的結束標記時回撥該方法。 下面第9行,獲取匹配規則;22行,觸發rule的body方法,此時是順序的;43行,觸發rule的end方法,此時,是逆序的!

 1     public void endElement( String namespaceURI, String localName, String qName )
 2         throws SAXException
 3     {
 4 
 5         boolean debug = log.isDebugEnabled();
 6 
 7 
 8         // Fire "body" events for all relevant rules
 9         List<Rule> rules = matches.pop();
10         if ( ( rules != null ) && ( rules.size() > 0 ) )
11         {
12             String bodyText = this.bodyText.toString();
13             Substitutor substitutor = getSubstitutor();
14             if ( substitutor != null )
15             {
16                 bodyText = substitutor.substitute( bodyText );
17             }
18             for ( int i = 0; i < rules.size(); i++ )
19             {
20 
21                     Rule rule = rules.get( i );
22                     rule.body( namespaceURI, name, bodyText );
23               
24             }
25         }
26 
27         // Recover the body text from the surrounding element
28         bodyText = bodyTexts.pop();
29 
30         // Fire "end" events for all relevant rules in reverse order
31         if ( rules != null )
32         {
33             for ( int i = 0; i < rules.size(); i++ )
34             {
35                 int j = ( rules.size() - i ) - 1;
36                 try
37                 {
38                     Rule rule = rules.get( j );
43 rule.end( namespaceURI, name ); 44 } 45 catch ( Exception e ) 46 { 47 log.error( "End event threw exception", e ); 48 throw createSAXException( e ); 49 } 50 catch ( Error e ) 51 { 52 log.error( "End event threw error", e ); 53 throw e; 54 } 55 } 56 } 57 58 // Recover the previous match expression 59 int slash = match.lastIndexOf( '/' ); 60 if ( slash >= 0 ) 61 { 62 match = match.substring( 0, slash ); 63 } 64 else 65 { 66 match = ""; 67 } 68 }

 

6、處理規則

處理規則就是前面我們看到的rule。rule的目的就是定義當模式匹配成功時,程式需要做什麼。

正式來講,一條處理規則就是一個實現了 org.apache.commons.digester.Rule 介面的java 類。每個Rule 實現下面的一個或多個方法,這些方法將在特定的時候被觸發:

begin() 當遇到匹配元素的開始標記時觸發。傳入引數包括元素相應的所有屬性
body() 當遇到匹配元素的正文內容時觸發。頭尾空格都會被移除
end() 當遇到匹配元素的結束標記時觸發。如果有內嵌的xml元素,會先觸發內嵌的xml元素的rule
finish() 當匹配元素的解析結束時,提供給程式清理快取或者臨時資料的機會

 

 

 

 

 

 

 

 

 

 

當你在配置Digester時,可以呼叫addRule()方法來給一個特定元素建立一條規則,該機制允許你建立自己的rule,增強程式的靈活性。

注:org.apache.commons.digester3.Digester 中 addRule 的簽名如下:

1     public void addRule( String pattern, Rule rule )
2     {
3         rule.setDigester( this );
4         getRules().add( pattern, rule );
5     }

 

當然,Digester已經給我們預定義了一堆規則,基本上能覆蓋很多的場景了。這些規則包括:

ObjectCreateRule 當begin方法被呼叫時,該規則會初始化一個指定java類的例項,並壓入棧中。要例項化的java類的類名,從xml元素的屬性中獲取,其屬性名需要從該Rule的建構函式中傳入。當end()方法被呼叫時,彈出棧頂元素。
FactoryCreateRule  ObjectCreateRule的變體,當要建立的java 類沒有無參建構函式時被呼叫。
SetPropertiesRule 當begin方法被呼叫時,digester使用java反射,根據xml元素中的屬性,來給棧頂的對應的 java 物件的屬性賦值。
SetNextRule  當end()被呼叫時,在棧頂物件的下一個物件上,呼叫指定的方法,(方法名通過建構函式傳入),引數為棧頂物件。通常用於建立parent-child關係。
CallMethodRule 當end()被呼叫時,在棧頂物件上呼叫指定的方法,方法名和引數個數需要在建構函式中指定。具體可參考上文中:ServletBean 的例子
CallParamRule  和CallMethodRule 配合使用,指定要使用的引數,引數將被加入digester 的另一個棧中(不同於物件棧),該棧只存放引數。具體可參考上文中:ServletBean 的例子

 

 

 

 

 

 

 

三、原始碼與總結

我個人而言,感覺Digester確實是神器,因為我們現在用的很多框架,其配置檔案都是xml,當然,這些年,註解很流行,但是xml依然沒有失去它的光彩。像我現在公司的Java EE專案,部分新專案,都用註解了,但是還是有一些部分是xml的,比如logback.xml、以及checkstyle等工具的配置檔案、Jrebel預設生成的配置檔案、Tomcat的配置檔案等。

xml和程式碼比,有什麼優勢,主要是方便修改,改後不需要重新再編譯。掌握了xml,基本就是可以自己折騰一些小工具,仿寫一些框架了。而Digester,就是那件輔助我們去造輪子的神器。

 

程式碼在:https://github.com/cctvckl/tomcat-saxtest  (也包括了前兩篇文章的程式碼)

如果有幫助,大家幫忙點個推薦

 

相關文章