一、属性
XPathParser
核心功能是封装了XPath
,对表达式进行解析,并转化成为指定的数据类型,其属性如下:
private Document document;
private boolean validation;
private EntityResolver entityResolver;
private Properties variables;
private XPath xpath;
document:要解析的xml文件被转化成的Document对象。
validation:获取document时是否要开启校验,开启校验的话会根据xml配置文件中定义的dtd文件校验xml格式,默认不开启校验。
entityResolver:实体解析器,用于从本地工程目录中引入dtd文件,而不是根据网络URL去加载校验文件。
variables:mybatis-config.xml配置文件中,
xpath:封装的XPath对象,用来解析表达式。
二、构造函数
XPathParser
提供了非常丰富的各种构造函数,支持通过多种形式来解析xml文件,可以传入xml的路径,可以先将xml处理成Reader
、InputStream
的形式,也可以将xml直接处理成Document
,所有构造函数的参数列表如下:
所有构造函数做的事都如下所示:
public XPathParser(InputStream inputStream, boolean validation, Properties variables) {
commonConstructor(validation, variables, null);
this.document = createDocument(new InputSource(inputStream));
}
commonConstruct()
方法初始化了除Document
之外的其他类属性,XPath
是通过XPathFactory
创建的,源码如下:
// 2. 初始化对象成员
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.variables = variables;
this.entityResolver = entityResolver;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
createDocument()
方法源码如下:
// 3. 将InputSource对象转化为Document对象
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation); // 是否要校验由外界传入的值确定
factory.setNamespaceAware(false); // 如果要使用mybatis的XSD Schema,此处必须设为true,但源码里是false,说明官方默认用dtd来做校验,舍弃了XSD Schema
factory.setIgnoringComments(true); // 默认忽略注释
factory.setIgnoringElementContentWhitespace(false); // 只有开启校验才能去除xml中的空白节点,但是不知是否开启校验,所以这里设为了false
factory.setCoalescing(false);
factory.setExpandEntityReferences(true); // 默认开启使用扩展实体引用
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver); // 使用传入的EntityResolver对象
builder.setErrorHandler(new ErrorHandler() { // 定义解析xml文档的错误处理器,如果发生了错误或致命错误则直接抛出异常,如果是警告默认不做处理
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {}
});
return builder.parse(inputSource);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
【源码解析】
要解析一个xml文件为Document
,需要DocumentBuilder
类的支持,实际上DocumentBuilder
本身就支持多种形式的xml的解析(可看末尾的附录),这里之所以先统一转成InputSource
,大概是为了代码简洁一点吧!
DocumentBuilder
对象由DocumentBuilderFactory
创建,DocumentBuilderFactory
可以设置是否校验、是否开启命名空间、是否忽略注释空白节点等,详细可以看上面源码注释。
如果开启了xml格式校验,DocumentBuilder
就要设置实体解析器,这里使用了构造函数传入的实体解析器,如果解析xml过程中检测到格式不对或者其他报错,则需要抛出异常信息,所以这里又设置了错误处理器ErrorHandler
,对于错误和致命错误直接抛出异常,对于警告则忽略。
三、方法功能
通过上面的构造,接下来可以根据表达式解析获取document
中的特定节点了,XPathParser
提供了eval() 系列方法,根据需要调用指定类型的eval方法,所有的eval()方法都是对evaluate()
方法返回值的处理,该方法中调用了XPath
解析表达式,源码如下:
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
大多数eval*()都是返回一个常规Java数据类型的包装类,eg:
public Boolean evalBoolean(String expression) {
return evalBoolean(document, expression);
}
public Boolean evalBoolean(Object root, String expression) {
return (Boolean) evaluate(expression, root, XPathConstants.BOOLEAN);
}
evalString()
稍有特殊,多了个解析占位符的不走,在[MyBatis源码详解 - 解析器模块 - 组件一] XNode中第四节分析getBodyData()
方法时已有说明,源码如下:
// [start]
public String evalString(String expression) {
return evalString(document, expression);
}
// 解析节点占位变量的实际值
// eg: ${database.password}
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
result = PropertyParser.parse(result, variables);
return result;
}
XPathParser
还支持解析文档中的节点并将其封装成XNode
对象,如果表达式表示的是一个节点,则调用evalNode()
方法;如果表达式表示的是一组节点,则调用evalNodes()
方法,源码如下:
public List evalNodes(String expression) {
return evalNodes(document, expression);
}
public List evalNodes(Object root, String expression) {
List xnodes = new ArrayList();
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
} else {
return new XNode(this, node, variables);
}
}
四、测试案例
对于eval*()
系列方法,在[MyBatis源码详解 - 解析器模块 - 组件一] XNode中的测试案例已经包含了该部分测试内容,此处不再赘叙,重点测试构造函数解析获取document
属性的过程,使用简单的模式,不校验xml格式,测试用xml文件内容如下(从学习Mybatis时写的代码中搬过来的,具体配置忽略,注重构建document
过程即可):
测试案例代码如下,跑案例时在new XPathParser
的地方打个断点,跟进入看看,就能知道构造XPathPserser
的完整流程。
public class XPathParserTest {
public static void main(String[] args) {
// 如果XML文件直接放在源目录(Source Directory)下,则可以直接解析
// 如果放在源目录下的子目录中,则必须加上相对工程路径
// InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("org/apache/ibatis/parsing/test/mybatis-config.xml");
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml");
XPathParser xPathParser = new XPathParser(inputStream, false, null, null);
System.out.println(xPathParser);
}
}
五、我的github(注释源码、测试案例)
[仓库地址] huyihao/mybatis-source-analysis
[注释源码] XPathParser.java
[测试案例源码] XPathParserTest.java