一、前言
我寫部落格主要靠自己實戰,理論知識不是很強,要全面介紹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 (也包括了前兩篇文章的程式碼)
如果有幫助,大家幫忙點個推薦