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