曹工說Tomcat3:深入理解 Tomcat Digester


一、前言

我寫博客主要靠自己實戰,理論知識不是很強,要全面介紹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  (也包括了前兩篇文章的代碼)

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM