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

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

  • 相关阅读:
    静态链接到 MFC 的规则 DLL
    DLLAFX_MANAGE_STATE(AfxGetStaticModuleState())模块状态切换保护
    多线程专题之线程同步(1)
    UVa 4256 Salesmen dp
    nefu 661 Clockwise 水DP+略几何
    10635 Prince and Princess LCS&LIS
    UVa 3882 And Then There Was One 递推无力orz
    UVa 10192 Vacation 字符串dp
    UVa 11584 Partitioning by Palindromes 回文串dp
    UVa 10534 Wavio Sequence LIS
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/11125584.html
Copyright © 2020-2023  润新知