一、前言
我写博客主要靠自己实战,理论知识不是很强,要全面介绍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、当遇到最外层的
2、基于xml元素的属性,来设置栈顶对象的属性。(比如此时栈顶对象为foo)
3、当遇到内嵌的
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>actionservlet-name>
<servlet-class>org.apache.struts.action.ActionServletservlet-class>
<init-param>
<param-name>applicationparam-name>
<param-value>org.apache.struts.example.ApplicationResourcesparam-value>
init-param>
<init-param>
<param-name>configparam-name>
<param-value>/WEB-INF/struts-config.xmlparam-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 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/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”可以匹配任意的标签,而不论其嵌套层次如何。当然,很有可能的是,当解析一个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 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 (也包括了前两篇文章的代码)
如果有帮助,大家帮忙点个推荐