曹工说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  (也包括了前两篇文章的代码)

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

 

posted @ 2019-07-03 14:44  三国梦回  阅读(1171)  评论(0编辑  收藏  举报