曹工说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文档即可:
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的更多相关文章
- 【曹工杂谈】Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗
Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗 前奏 我们上篇文章,跟大家说了下,怎么调试maven插件的代码,注意,是插件的代码.插件,是要让主框架来执行的,主框架是谁呢,就是maven ...
- 【曹工杂谈】Maven源码调试工程搭建
Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...
- 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Redis源码(4)-- 通过redis server源码来理解 listen 函数中的 backlog 参数
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器
一.前言 一共8个类,撸一个IOC容器.当然,我们是很轻量级的,但能够满足基本需求.想想典型的 Spring 项目,是不是就是各种Service/DAO/Controller,大家互相注入,就组装成了 ...
- 【曹工杂谈】Mysql-Connector-Java时区问题的一点理解--写入数据库的时间总是晚13小时问题
背景 去年写了一篇"[曹工杂谈]Mysql客户端上,时间为啥和本地差了整整13个小时,就离谱",结果最近还真就用上了. 不是我用上,是组内一位同事,他也是这样:有个服务往数据库in ...
- 曹工说Tomcat1:从XML解析说起
一.前言 第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主13年校招进华为,14年在东莞出差,给东莞移动的通信设备进行版本更新.他们那边的一个小伙子来接我的时候,这么叫我的,刚听到的时候,心里一 ...
- 曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
随机推荐
- 工作流管理平台Airflow
Airflow 1. 引言 Airflow是Airbnb开源的一个用Python写就的工作流管理平台(workflow management platform).在前一篇文章中,介绍了如何用Cront ...
- Codeforces 15C Industrial Nim 简单的游戏
主题链接:点击打开链接 意甲冠军: 特定n 下列n行,每一行2的数量u v 表达v礧:u,u+1,u+2···u+v-1 问先手必胜还是后手必胜 思路: 首先依据Nim的博弈结论 把全部数都异或一下, ...
- 1 开始ThreeJs
因为需要 需要一款 网页上的 游戏引擎 通过百度知道了 three.js 1.先从github上clone下源码 https://github.com/mrdoob/three.js 2.下载web ...
- Win7 64位系统,使用(IME)模式VS2010 编写 和 安装 输入法 教程(1)
原文:Win7 64位系统,使用(IME)模式VS2010 编写 和 安装 输入法 教程(1) 首先感谢:http://blog.csdn.net/shuilan0066/article/detail ...
- ZOJ 3819 Average Score(数学 牡丹江游戏网站)
主题链接:problemId=5373">http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5373 Bob is ...
- java的System.getProperty()值的方法可以得到
java.version Java 执行时环境版本号 java.vendor Java 执行时环境供应商 java.vendor.url Java 供应商的 URL java.home Java 安装 ...
- js错误界面
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...
- C# Winform制作虚拟键盘,支持中文
原文:C# Winform制作虚拟键盘,支持中文 最近在做一个虚拟键盘功能,代替鼠标键盘操作,效果如下: 实现思路: 1 构建中文-拼音 数据库, ...
- SVG路径动画解密
原文:SVG路径动画解密 原文链接:http://www.gbtags.com/gb/share/5581.htm SVG路径动画效果现在貌似越来越多网站都使用了,给我的感觉就像是一段时间的流行而已, ...
- Win8Metro(C#)数字图像处理--2.29图像除法运算
原文:Win8Metro(C#)数字图像处理--2.29图像除法运算 [函数名称] 图像除法函数DivisionProcess(WriteableBitmap src, WriteableBit ...