前面我们介绍过如何 手写一个XML解析器。实际上我们就是按照Digester的基本原理来实现的。下面我们根据Digester的源码再来回顾一遍。

源码结构

digester源码结构图

  • DefaultHandler:由SAX提供,Digester继承自该类。
  • Digester:事件处理器。组件核心类。基于SAX封装了一系列对使用者友好的API
  • Rules:接口,抽象了对解析规则的添加、删除和匹配等方法。
  • AbstractRulesImplRules接口的抽象实现,实际没什么实现,主要用来维护一些公共属性。
  • RulesBaseDigester使用的Rules接口的默认实现类。实际是一个工厂,内部用一个HashMap来维护XML节点和解析规则的映射关系。
  • Rule:解析规则接口。抽象了解析XML的过程(生命周期)。
  • ObjectCreateRuleRule接口的实现类。提供创建对象功能。
  • SetPropertiesRuleRule接口的实现类。提供对象属性赋值功能。
  • SetNextRuleRule接口的实现类。提供对象方法调用功能。

回顾一下我们自己手写的XML解析器。Rule规则接口相当于我们的Processor策略接口,RulesBase相当于我们的ProcessorFactory策略工厂。整体的架构体现了策略+工厂模式。

使用示例

回顾一下我们的使用方法:

1
2
3
4
5
6
7
8
// 创建Digester对象
Digester digester = createDigester();
// 获取xml文件的输入流
InputStream is = DigesterParseXmlTest.class.getResourceAsStream("/xml/server.xml");
// 将当前类压入Digester的对象栈栈顶
digester.push(this);
// 执行解析
digester.parse(is);

其中createDigester()方法中指定了一系列的解析规则。digester.push(this);会将当前对象压入对象栈顶,对象栈的底层数据结构是java.util.StackDigester封装了一些空指针安全的操作对象栈的方法以供使用。主要有以下方法:

  • push(T object):压入一个元素至栈顶。
  • pop():弹出栈顶元素。
  • peek():获取栈顶元素的引用(元素不会出栈)。
  • peek(int n):获取栈中从栈顶到栈底索引位置为n的元素的引用(元素不会出栈)。

解析规则:XML节点的生命周期

DigesterRule类抽象了解析整个XML文件节点的生命周期,包含以下几个阶段:

  • begin:解析到匹配节点的开始标签部分。
  • body:解析到匹配节点的普通文本内容。
  • end:解析到匹配节点的结束标签部分。此处end方法的整体调用顺序应该是先begin的标签后end
  • finish:解析完所有的节点。

Rule类是一个抽象类,这些生命周期方法都是空方法,需要由子类进行具体实现。

解析规则实现

Digester包中提供的一些常用解析规则实现。

  • CreateObjectRulebegin方法会将指定的类实例化并压入对象栈顶。具体的类可在该规则类的构造方法中指定;也可通过当前解析节点的某个属性指定,属性名称通过构造方法传入。end方法会将创建的对象从对象栈中弹出。
  • SetPropertiesRulebegin方法会使用标准Java Bean属性操作方法(setter方法)将当前解析节点的属性值设置到对象栈的栈顶元素中。
  • SetNextRuleend方法会将栈顶元素作为入参,调用栈顶元素的下一个元素的指定方法。一般用于建立对象的“父子”关系,形成对象引用树。
  • CallMethodRuleend方法会调用对象栈的栈顶元素的指定方法。

这些解析规则的实现原理和前面 手写一个XML解析器 中的一样,都是通过反射来实现的。

自定义规则

Digester包提供的Rule解析规则无法满足需求时,可进行规则的自定义。只需要继承Rule抽象类或其相关子类,并重写需要的生命周期方法即可。

例如:

1
2
3
4
5
6
public class ListenerCreateRule extends ObjectCreateRule {
@Override
public void begin(String namespace, String name, Attributes attributes) throws Exception {
// do something
}
}
1
2
3
4
5
6
7
8
9
10
11
public class ConnectorCreateRule extends Rule {
@Override
public void begin(String namespace, String name, Attributes attributes) throws Exception {
// do something
}

@Override
public void end(String namespace, String name) throws Exception {
// do something
}
}

addRuleSet

针对相同节点,但是其父节点不同的情况,我们的解析规则定义会出现重复。这时我们可以将重复的解析规则定义成一个RuleSet进行复用。

RuleSet接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface RuleSet {

/**
* Add the set of Rule instances defined in this RuleSet to the
* specified <code>Digester</code> instance, associating them with
* our namespace URI (if any). This method should only be called
* by a Digester instance.
*
* @param digester Digester instance to which the new Rule instances
* should be added.
*/
void addRuleInstances(Digester digester);
}

实现类需要有一个prefix前缀来匹配不同的父节点,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NamingRuleSet implements RuleSet {
protected final String prefix;

public NamingRuleSet() {
this("");
}

public NamingRuleSet(String prefix) {
this.prefix = prefix;
}

@Override
public void addRuleInstances(Digester digester) {
// do add rule
digester.addObjectCreate(prefix + "Ejb",
"org.apache.tomcat.util.descriptor.web.ContextEjb");
// ...
}
}

在使用时通过传入不同的前缀来匹配父节点不同的节点,例如:

1
2
digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));

上面的规则会分别匹配到Server/GlobalNamingResources/下的Ejb节点和Server/Service/Engine/Host/Context/下的Ejb节点。这样我们就复用了解析规则的定义。