什么是digester?解决了什么问题?
在tomcat里,digester就是一个解析xml的工具,用来解析server.xml来创建应用服务器。
digester本来仅仅是Jakarta Struts中的一个工具,用于处理struts-config.xml配置文件。由于将XML文件转换成相应的Java对象是一项很通用的功能,后来又被移到了Apache Common项目中。但在Tomcat中并不依赖Apache common,而是将Digester源码包含了进来,并改变了包路径。
digester底层以来SAX(Simple API for XML),采用事件驱动的方式将xml配置转换为java对象,是对SAX的高层次封装。
SAX是什么?
SAX是一个事件驱动的xml解析框架,与之类似的xml解析框架还有DOM、JDOM、DOM4J等。SAX解析器不像DOM那样建立一个完整的文档树,而是在读取文档时激活一系列事件,这些事件被推给事件处理器,然后由事件处理器提供对文档内容的访问。
常见的事件处理器有三种基本类型:
- 用于访问XML DTD内容的DTDHandler;
- 用于低级访问解析错误的ErrorHandler;
- 用于访问文档内容的ContentHandler,这也是最普遍使用的事件处理器,一般也是最关注的
例如由如下xml,xml的一个element可以可以包含三种内容
- attribute。department包含name和code属性,user也包含name和code属性
- 子element。department包含user、extension子element,extension包含property-name、property-value两个子element
- body。property-name和property-value具有body,分别是director、joke
<department name="deptname001" code="deptcode001">
<user name="username001" code ="usercode001"/>
<user name="username002" code ="usercode002"/>
<extension>
<property-name>director</property-name>
<property-value>joke</property-value>
</extension>
</department>
ContentHandler是一个接口,解析xml遇到各种事件时,会调用ContentHandler的相应回调接口,其中以下五个方法比较关注
- startDocument 。开始解析xml的回调,在这里可以处理一些资源准备的工作
- endDocument。xml解析结束的回调,在这里可以处理资源回收工作
- startElement。读取到element开始时的回调,atts是该element内携带的属性
- endElement。element读取结束的回调
- characters。读取到body的回调
下面实现一个ContentHandler(删除了空实现的方法),来处理上面的xml
public class LoggerContentHandler implements ContentHandler {
public static void main(String[] args) throws Exception {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.getXMLReader().setContentHandler(new LoggerContentHandler());
parser.getXMLReader().parse(new InputSource(Files.newInputStream(Paths.get("./test.xml"))));
}
@Override
public void startDocument() throws SAXException {
System.out.println("startDocument");
}
@Override
public void endDocument() throws SAXException {
System.out.println("endDocument");
}
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
Map<String, String> attrMap = new HashMap<>();
for (int i = 0; i < atts.getLength(); i++) {
String name = atts.getLocalName(i);
if (name.isEmpty()) {
name = atts.getQName(i);
}
String value = atts.getValue(i);
attrMap.put(name, value);
}
System.out.println("startElement, uri: " + uri + ", localName: " + localName + ", qName: " + qName +
", atts: " + attrMap);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println("endElement, uri: " + uri + ", localName: " + localName + ", qName: " + qName);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
System.out.println("characters: " + new String(ch, start, length));
}
}
输出如下
startDocument
startElement, uri: , localName: , qName: department, atts: {code=deptcode001, name=deptname001}
characters:
startElement, uri: , localName: , qName: user, atts: {code=usercode001, name=username001}
endElement, uri: , localName: , qName: user
characters:
startElement, uri: , localName: , qName: user, atts: {code=usercode002, name=username002}
endElement, uri: , localName: , qName: user
characters:
startElement, uri: , localName: , qName: extension, atts: {}
characters:
startElement, uri: , localName: , qName: property-name, atts: {}
characters: director
endElement, uri: , localName: , qName: property-name
characters:
startElement, uri: , localName: , qName: property-value, atts: {}
characters: joke
endElement, uri: , localName: , qName: property-value
characters:
endElement, uri: , localName: , qName: extension
characters:
endElement, uri: , localName: , qName: department
endDocument
digester是怎么实现的?
digester是SAX的高层次封装,类继承图如下
可以看到其本身就是一个ContentHandler,因此可以在xml解析的各时期收到回调事件,主要是接收startDocument、endDocument、startElement、endElement、characters这五个事件。这种级别上的抽象对于SAX来讲足够好了,但对于使用者来讲不够便利,主要有两点
- SAX是面向事件的,解析每个标签都会发出startElement、endElement、characters,怎么对不同标签进行不同的处理?
- 不同标签的处理往往需要创建不同的对象,然后设置不同属性,但创建对象、设置属性本身是通用的,怎么复用?
- 一个标签会包含属性、子标签、body等,处理子标签时当前标签的上下文怎么处理?
怎么从面向事件转为面向标签?
digester将对标签的不同操作抽象为rule,通过rules维护不同标签的处理操作。当digester解析到某个标签时,就能够获取到该标签的对应的rule,从而调用rules的方法处理当前标签
Rule是怎么抽象的?
Rule表示针对某个标签的具体的操作,digester给了一些默认的Rule
- ObjectCreateRule:当遇到标签开始时,会创建一个对象,push到对象栈里面;这个标签结束时会从栈里面弹出对象
- SetPropertiesRule:遇到标签开始时,会从标签中获取属性,填充到对象里
- SetNextRule:当遇到标签结束时,会将栈顶元素作为参数,调用栈顶第二个元素的指定方法
Rule有四个回调方法:
-
begin。开始解析一个标签时的回调,能够获取到这个标签的属性
-
body。标签结束时的回调,能够获取到这个标签里定义的body
-
end。标签解析结束的回调
-
finish。xml解析结束的回调
SAX回调什么时候会回调这几个方法呢? -
startElement事件:begin方法会被调用,传入标签的attribute
-
characters事件:digester只暂存body,不会调用rule方法
-
endElement事件:首先调用body方法,传入标签body;然后调用end方法,没有参数
-
endDocument事件:调用finish方法,rule可以做清理操作
上下文处理
digester的xml处理上下文是基于栈实现的。当处理标签/a时,在开始时会为/a标签创建上下文,当遇到子标签/a/b时,会将/a的上下文push到栈里面,再为/a/b创建上下文,当/a/b标签解析完之后,在从栈里面弹出上下文,继续处理/a标签。具体包括四种栈:
- 对象栈。标签开始时可以创建对象push到栈里面,标签结束后要弹出
- rule栈。标签开始时,会从rules里面找到当前标签对应的rule,push到栈里面暂存
- body栈。暂存每个标签的body内容
- 参数栈。暂存CallMethodRule的参数
具体就是Digester的几个属性
// 当前body
protected StringBuilder bodyText = new StringBuilder();
// body栈
protected ArrayStack<StringBuilder> bodyTexts = new ArrayStack<>();
// rule栈
protected ArrayStack<List<Rule>> matches = new ArrayStack<>(10);
// 参数栈
protected ArrayStack<Object> params = new ArrayStack<>();
// 暂存的根对象
protected Object root = null;
// 对象栈
protected ArrayStack<Object> stack = new ArrayStack<>();
代码实现
startElement方法(删除了无关代码)
public void startElement(String namespaceURI, String localName, String qName, Attributes list)
throws SAXException {
// 对Attribute做一下处理,替换一下占位符
list = updateAttributes(list);
// 一个新的标签的开始,需要暂存上一个标签的bodyText,并创建新的bodyText,用于接收当前标签的body内容
bodyTexts.push(bodyText);
bodyText = new StringBuilder();
// 获取当前标签名字
String name = localName;
if ((name == null) || (name.length() < 1)) {
name = qName;
}
// match记录的是当前的标签的处理路径,例如/dept/a。当新标签开始,就需要追加当前标签名字。
// 相应的,当标签解析结束时,就需要回退。这个逻辑在endElement方法里
StringBuilder sb = new StringBuilder(match);
if (match.length() > 0) {
sb.append('/');
}
sb.append(name);
match = sb.toString();
// 通过Rules获取处理当前标签的Rule,暂存到matches里,并调用每个rule的begin方法
List<Rule> rules = getRules().match(namespaceURI, match);
matches.push(rules);
if ((rules != null) && (rules.size() > 0)) {
for (Rule value : rules) {
try {
Rule rule = value;
rule.begin(namespaceURI, name, list);
} catch (Exception e) {}
}
}
}
characters方法(删除了无关代码)
public void characters(char buffer[], int start, int length) throws SAXException {
// 就是接收body内容,暂存起来
bodyText.append(buffer, start, length);
}
endElement方法(删除了无关代码)
public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {
// 解析body里面的占位符
bodyText = updateBodyText(bodyText);
// 提取当前标签名字
String name = localName;
if ((name == null) || (name.length() < 1)) {
name = qName;
}
// 从rule栈里拿出匹配上的rule。调用rule的body方法
List<Rule> rules = matches.pop();
if ((rules != null) && (rules.size() > 0)) {
String bodyText = this.bodyText.toString().intern();
for (Rule value : rules) {
try {
Rule rule = value;
rule.body(namespaceURI, name, bodyText);
} catch (Exception e) {}
}
}
// 标签结束,要将外层标签的bodyText弹出,恢复上下文
bodyText = bodyTexts.pop();
// 调用rule的end方法
if (rules != null) {
for (int i = 0; i < rules.size(); i++) {
int j = (rules.size() - i) - 1;
try {
Rule rule = rules.get(j);
rule.end(namespaceURI, name);
} catch (Exception e) {}
}
}
// 恢复match内容
int slash = match.lastIndexOf('/');
if (slash >= 0) {
match = match.substring(0, slash);
} else {
match = "";
}
}
endDocument方法(删除了无关代码)
public void endDocument() throws SAXException {
// 清空栈
while (getCount() > 1) {
pop();
}
// 调用所有rule的finish方法
for (Rule rule : getRules().rules()) {
try {
rule.finish();
} catch (Exception e) {}
}
clear();
}
这里面的对象栈跟参数栈digester并没有修改,而是由Rule去操作这些栈。对象栈由ObjectCreateRule操作、参数栈由CallMethodRule操作
More Details
ObjectCreateRule的实现
public class ObjectCreateRule extends Rule {
public ObjectCreateRule(String className) {
this(className, (String) null);
}
public ObjectCreateRule(String className,
String attributeName) {
this.className = className;
this.attributeName = attributeName;
}
// className就是传进来创建对象的类。attributeName就是获取className的属性名,可以覆盖className
protected String attributeName = null;
protected String className = null;
@Override
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
// 获取className,attributeName中获取的可以覆盖className
String realClassName = className;
if (attributeName != null) {
String value = attributes.getValue(attributeName);
if (value != null) {
realClassName = value;
}
}
// 实例化对象,push到对象栈里面
Class<?> clazz = digester.getClassLoader().loadClass(realClassName);
Object instance = clazz.getConstructor().newInstance();
digester.push(instance);
}
@Override
public void end(String namespace, String name) throws Exception {
// end调用时需要弹出当前对象
Object top = digester.pop();
}
}
SetNextRule的实现。SetNextRule解决每个标签单独创建对象但是不能相互关联的问题,就是将栈顶对象作为参数,通过调用栈里面第二个对象个指定方法进行关联的
public class SetNextRule extends Rule {
public SetNextRule(String methodName,
String paramType) {
this.methodName = methodName;
this.paramType = paramType;
}
// 指定的方法名跟参数类型
protected String methodName = null;
protected String paramType = null;
protected boolean useExactMatch = false;// getter and setter
@Override
public void end(String namespace, String name) throws Exception {
// 取出栈顶元素和第二个元素,调用相应方法
Object child = digester.peek(0);
Object parent = digester.peek(1);
IntrospectionUtils.callMethod1(parent, methodName,
child, paramType, digester.getClassLoader());
}
}
SetPropertiesRule用来设置栈顶对象的属性
public class SetPropertiesRule extends Rule {
@Override
public void begin(String namespace, String theName, Attributes attributes)
throws Exception {
// 获取栈顶对象
Object top = digester.peek();
// 遍历attribute,通过IntrospectionUtils.setProperty设置属性
for (int i = 0; i < attributes.getLength(); i++) {
String name = attributes.getLocalName(i);
if (name.isEmpty()) {
name = attributes.getQName(i);
}
String value = attributes.getValue(i);
if (!digester.isFakeAttribute(top, name)
&& !IntrospectionUtils.setProperty(top, name, value)
&& digester.getRulesValidation()) {
}
}
}
}
怎么持有根节点?
ObjectCreateRule在标签开始时创建对象,在标签结束时弹出对象,那么当xml解析完毕,根对象也就弹出去了,怎么持有根对象呢?有两种方式
第一种方式,digester的push方法会判断栈是否为空,空的话会暂存到root属性上,解析结束后可以通过root属性获取根对象
public void push(Object object) {
if (stack.size() == 0) {
root = object;
}
stack.push(object);
}
第二种方式,digester使用方push一个对象到栈里面,当作跟对象的“容器”,解析根对象的时候SetNextRule将根对象关联到“容器”里
catalina中digester的使用
catalina里通过digester解析server.xml(有删除)
protected Digester createStartDigester() {
Digester digester = new Digester();
// 1、创建Server,2、设置属性,3、将server关联到外部catalina实例(createStartDigester之后还会将Catalina实例push进去)
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
// 创建listener、设置属性、关联到Server
digester.addObjectCreate("Server/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Listener");
digester.addSetNext("Server/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
// 创建service、设置属性、关联到Server
digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService",
"className");
digester.addSetProperties("Server/Service");
digester.addSetNext("Server/Service",
"addService",
"org.apache.catalina.Service");
// 为service创建listener,关联到service
digester.addObjectCreate("Server/Service/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Service/Listener");
digester.addSetNext("Server/Service/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
//创建executor,关联到service
digester.addObjectCreate("Server/Service/Executor",
"org.apache.catalina.core.StandardThreadExecutor",
"className");
digester.addSetProperties("Server/Service/Executor");
digester.addSetNext("Server/Service/Executor",
"addExecutor",
"org.apache.catalina.Executor");
// 创建Connector,关联到Service
digester.addRule("Server/Service/Connector",
new ConnectorCreateRule());
digester.addRule("Server/Service/Connector",
new SetAllPropertiesRule(new String[]{"executor", "sslImplementationName"}));
digester.addSetNext("Server/Service/Connector",
"addConnector",
"org.apache.catalina.connector.Connector");
// 创建Listener,关联到Connector
digester.addObjectCreate("Server/Service/Connector/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Service/Connector/Listener");
digester.addSetNext("Server/Service/Connector/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
// 为Engine、Host、Context创建Rule
digester.addRuleSet(new EngineRuleSet("Server/Service/"));
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
return digester;
}