一、前言

我写博客主要靠自己实战,理论知识不是很强,要全面介绍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文档即可:

   Digester digester = new Digester();
digester.setValidating(false);
digester.addObjectCreate("foo", "mypackage.Foo");
digester.addSetProperties("foo");
digester.addObjectCreate("foo/bar", "mypackage.Bar");
digester.addSetProperties("foo/bar");
digester.addSetNext("foo/bar", "addBar", "mypackage.Bar");
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不好理解,大家参考以下实现代码就理解了:

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

一旦解析完成,首个被压入栈内的对象将被返回。此时,该对象的所有属性及子元素都已被设置,程序可以拿来用了。

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 的例子。

     protected void initServlet()
// Remember our servlet name
this.servletName = getServletConfig().getServletName(); // Prepare a Digester to scan the web application deployment descriptor
Digester digester = new Digester(); digester.push(this);
digester.setNamespaceAware(true);
digester.setValidating(false); // Register our local copy of the DTDs that we can find
for (int i = 0; i < registrations.length; i += 2) {
URL url = this.getClass().getResource(registrations[i + 1]); if (url != null) {
18 digester.register(registrations[i], url.toString());
}
} // Configure the processing rules that we need
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);
InputStream input =
getServletContext().getResourceAsStream("/WEB-INF/web.xml");
digester.parse(input); }

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 如下:

 package mypackage;

 import lombok.Data;

 import java.util.ArrayList;
import java.util.List; @Data
public class ServletBean {
private String servletName;
private String servletClass; private List<InitParam> initParams = new ArrayList<>(); public void addInitParam(String name, String value){
initParams.add(new InitParam(name,value));
} }
 package mypackage;

 import lombok.AllArgsConstructor;
import lombok.Data; @Data
@AllArgsConstructor
public class InitParam {
private String name; private String value; }

解析代码如下所示:

 package mypackage;

 import org.apache.commons.digester3.Digester;
import org.xml.sax.SAXException; import java.io.IOException;
import java.io.InputStream; public class WebXmlParseTest {
public static void main(String[] args) {
Digester digester = new Digester();
digester.setValidating(false); digester.addObjectCreate("web-app/servlet",
"mypackage.ServletBean");
digester.addCallMethod("web-app/servlet/servlet-name", "setServletName", 0);
digester.addCallMethod("web-app/servlet/servlet-class",
"setServletClass", 0);
digester.addCallMethod("web-app/servlet/init-param",
"addInitParam", 2);
digester.addCallParam("web-app/servlet/init-param/param-name", 0);
digester.addCallParam("web-app/servlet/init-param/param-value", 1); InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("web.xml");
try {
ServletBean servletBean = (ServletBean) digester.parse(inputStream);
System.out.println(servletBean);
} catch (IOException | SAXException e) {
e.printStackTrace();
}
}
}

执行效果如下:

注:说实话,这个真的相当方便,很多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>元素时被匹配。同样,文档内出现多少次,该模式就被匹配多少次。

我们以例子说话:

  <a>         -- Matches pattern "a"
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
</a>

当然,我们也可以匹配某一个特定的元素,而不管它被嵌套在哪一层,要达到这个目的,只需要使用 “*” 即可。比如,“*/a”可以匹配任意的<a>标签,而不论其嵌套层次如何。当然,很有可能的是,当解析一个xml文档时,我们给一个模式注册了多个规则。当这种情况发生时,多个规则都能得到匹配(注:就像前面我们的代码里示例的一样),此时,在触发 rule 的 begin 和 body 方法时(在解析到xml开始标记和元素内容时触发),相应的解析规则会按照顺序触发;但是,在解析到xml的结束标记时,触发 rule 的end方法时,会按照相反的顺序触发。

注:以下即为Digester的endElement方法,在xml解析到元素的结束标记时回调该方法。 下面第9行,获取匹配规则;22行,触发rule的body方法,此时是顺序的;43行,触发rule的end方法,此时,是逆序的!

     public void endElement( String namespaceURI, String localName, String qName )
throws SAXException
{ boolean debug = log.isDebugEnabled(); // Fire "body" events for all relevant rules
9 List<Rule> rules = matches.pop();
if ( ( rules != null ) && ( rules.size() > 0 ) )
{
String bodyText = this.bodyText.toString();
Substitutor substitutor = getSubstitutor();
if ( substitutor != null )
{
bodyText = substitutor.substitute( bodyText );
}
for ( int i = 0; i < rules.size(); i++ )
{ Rule rule = rules.get( i );
22 rule.body( namespaceURI, name, bodyText ); }
} // Recover the body text from the surrounding element
bodyText = bodyTexts.pop(); // Fire "end" events for all relevant rules in reverse order
if ( rules != null )
{
for ( int i = 0; i < rules.size(); i++ )
{
int j = ( rules.size() - i ) - 1;
try
{
Rule rule = rules.get( j );
43 rule.end( namespaceURI, name );
}
catch ( Exception e )
{
log.error( "End event threw exception", e );
throw createSAXException( e );
}
catch ( Error e )
{
log.error( "End event threw error", e );
throw e;
}
}
} // Recover the previous match expression
int slash = match.lastIndexOf( '/' );
if ( slash >= 0 )
{
match = match.substring( 0, slash );
}
else
{
match = "";
}
}

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 的签名如下:

     public void addRule( String pattern, Rule rule )
{
rule.setDigester( this );
getRules().add( pattern, rule );
}

当然,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  (也包括了前两篇文章的代码)

如果有帮助,大家帮忙点个推荐

曹工说Tomcat3:深入理解 Tomcat Digester的更多相关文章

  1. 【曹工杂谈】Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗

    Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗 前奏 我们上篇文章,跟大家说了下,怎么调试maven插件的代码,注意,是插件的代码.插件,是要让主框架来执行的,主框架是谁呢,就是maven ...

  2. 【曹工杂谈】Maven源码调试工程搭建

    Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...

  3. 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 曹工说Redis源码(4)-- 通过redis server源码来理解 listen 函数中的 backlog 参数

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  5. 曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器

    一.前言 一共8个类,撸一个IOC容器.当然,我们是很轻量级的,但能够满足基本需求.想想典型的 Spring 项目,是不是就是各种Service/DAO/Controller,大家互相注入,就组装成了 ...

  6. 【曹工杂谈】Mysql-Connector-Java时区问题的一点理解--写入数据库的时间总是晚13小时问题

    背景 去年写了一篇"[曹工杂谈]Mysql客户端上,时间为啥和本地差了整整13个小时,就离谱",结果最近还真就用上了. 不是我用上,是组内一位同事,他也是这样:有个服务往数据库in ...

  7. 曹工说Tomcat1:从XML解析说起

    一.前言 第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主13年校招进华为,14年在东莞出差,给东莞移动的通信设备进行版本更新.他们那边的一个小伙子来接我的时候,这么叫我的,刚听到的时候,心里一 ...

  8. 曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. 工作流管理平台Airflow

    Airflow 1. 引言 Airflow是Airbnb开源的一个用Python写就的工作流管理平台(workflow management platform).在前一篇文章中,介绍了如何用Cront ...

  2. Codeforces 15C Industrial Nim 简单的游戏

    主题链接:点击打开链接 意甲冠军: 特定n 下列n行,每一行2的数量u v 表达v礧:u,u+1,u+2···u+v-1 问先手必胜还是后手必胜 思路: 首先依据Nim的博弈结论 把全部数都异或一下, ...

  3. 1 开始ThreeJs

    因为需要 需要一款 网页上的 游戏引擎 通过百度知道了 three.js 1.先从github上clone下源码  https://github.com/mrdoob/three.js 2.下载web ...

  4. Win7 64位系统,使用(IME)模式VS2010 编写 和 安装 输入法 教程(1)

    原文:Win7 64位系统,使用(IME)模式VS2010 编写 和 安装 输入法 教程(1) 首先感谢:http://blog.csdn.net/shuilan0066/article/detail ...

  5. ZOJ 3819 Average Score(数学 牡丹江游戏网站)

    主题链接:problemId=5373">http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5373 Bob is ...

  6. java的System.getProperty()值的方法可以得到

    java.version Java 执行时环境版本号 java.vendor Java 执行时环境供应商 java.vendor.url Java 供应商的 URL java.home Java 安装 ...

  7. js错误界面

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  8. C# Winform制作虚拟键盘,支持中文

    原文:C# Winform制作虚拟键盘,支持中文           最近在做一个虚拟键盘功能,代替鼠标键盘操作,效果如下:        实现思路:          1  构建中文-拼音 数据库, ...

  9. SVG路径动画解密

    原文:SVG路径动画解密 原文链接:http://www.gbtags.com/gb/share/5581.htm SVG路径动画效果现在貌似越来越多网站都使用了,给我的感觉就像是一段时间的流行而已, ...

  10. Win8Metro(C#)数字图像处理--2.29图像除法运算

    原文:Win8Metro(C#)数字图像处理--2.29图像除法运算  [函数名称] 图像除法函数DivisionProcess(WriteableBitmap src, WriteableBit ...