(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨
👀👀👀 个人博客:小奥的博客
👍👍👍:个人CSDN
⭐️⭐️⭐️:传送门
🍹 本人24应届生一枚,技术和水平有限,如果文章中有不正确的内容,欢迎多多指正!
📜 欢迎点赞收藏关注哟! ❤️
解析器模块
解析器模块主要提供了两个功能:
- 为解析MyBatis配置文件、Mapper映射文件等提供支持
- 为处理动态SQL语句中的占位符提供支持
MyBatis的解析器模块目录如下:
主要包路径:org.apache.ibatis.parsing
解析器模块的包结构如上图所示,其中,XPathParser 和XNode 主要用来解析 XML,PropertyParser、GenericTokenParser 、TokenHandler 主要用于标记处理(占位符),ParsingException是异常处理类。
GenericTokenParser
:通用的Token解析器(即占位符解析器),解析xml文件中的占位符${}
,并返回对应的值ParsingException
:异常处理类PropertyParser
:动态属性解析器TokenHandler
:标记处理接口XNode
:XML节点,封装了NodeXPathParser
:基于Java XPath 解析器,用于解析MyBatis的mybatis-config.xml
和Mapper.xml
等XML配置文件
① TokenHandler
TokenHandler是一个接口,定义了一个handleToken方法,该方法主要用于标记处理,具体实现在各自实现类中完成。主要是配合通用标记解析器GenericTokenParser类完成对配置文件等的解析工作,其中TokenHandler主要完成处理,而解析器主要实现前序工作——解析。
public interface TokenHandler {
/**
* 标记处理接口
* @param content
* @return
*/
String handleToken(String content);
}
② XNode
XNode作为解析xml配置文件的基础类,其中封装了Node节点,并且提供了常见的解析一个Node节点需要的功能和方法。
public class XNode {
private final Node node; // mark 被包装的org.w3c.dom.Node对象
private final String name; // mark 节点名称
private final String body; // mark 节点内容
private final Properties attributes; // mark 节点属性集合
private final Properties variables; // mark 配置文件中<properties>节点下定义的键值对
private final XPathParser xpathParser; // mark 封装了XPath解析器,负责XNode对象的生成,并提供解析XPath表达式的功能
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
this.attributes = parseAttributes(node);
this.body = parseBody(node);
}
public XNode newXNode(Node node) {
return new XNode(xpathParser, node, variables); // mark 根据传入的Node对象,创建XNode对象实例
}
/**
* 获取父节点
* @return 父节点是Element类型,返回封装好的XNode节点,否则返回null
*/
public XNode getParent() {
Node parent = node.getParentNode();
if (!(parent instanceof Element)) {
return null;
}
return new XNode(xpathParser, parent, variables);
}
/**
* 获取节点路径
* @return 节点的路径值
* 比如 <A><B><C><C/><B/><A/>,对于C节点来说节点路径就是 A/B/C
*/
public String getPath() {
StringBuilder builder = new StringBuilder();
Node current = node; // mark 当前节点
while (current instanceof Element) {
if (current != node) {
builder.insert(0, "/");
}
builder.insert(0, current.getNodeName());
current = current.getParentNode(); // mark 向上追溯节点,直到顶层节点
}
return builder.toString();
}
/**
* 获取节点的识别码
* @return 返回唯一标识字符串,如下面的C节点的唯一标识字符串就是 A_B[bid]_C[cid]
* @Code <A>
* <B id="bid">
* <C id="cid" value="cvalue"/>
* </B>
* </A>
*/
public String getValueBasedIdentifier() {
StringBuilder builder = new StringBuilder();
XNode current = this;
while (current != null) {
if (current != this) {
builder.insert(0, "_");
}
String value = current.getStringAttribute("id",
current.getStringAttribute("value", current.getStringAttribute("property", (String) null)));
if (value != null) {
value = value.replace('.', '_');
builder.insert(0, "]");
builder.insert(0, value);
builder.insert(0, "[");
}
builder.insert(0, current.getName());
current = current.getParent();
}
return builder.toString();
}
/**
* evalXXX()方法,调用XPathParser方法在当前节点下寻找符合表达式条件的节点,通常是文本节点,并
* 将值转为为指定的类型,如果值无法转化为指定类型,则会报错。
* 【支持数据类型】 String、Boolean、Double、Node、List<Node>
*/
...
/**
* getXXXBody()方法:获取文本内容并将其转化为指定的数据类型
* 【支持的数据类型】String、Boolean、Integer、Long、Double、Float
*/
...
/**
* getXxxAttribute()方法:获取节点指定属性的属性值并将其转化为指定的数据类型
*/
...
/**
* 获取子节点,对Node.getChildNodes()做相应的封装得到List<XNode>
* @return 子节点集合
*/
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<>();
NodeList nodeList = node.getChildNodes();
if (nodeList != null) {
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
/**
* 获取所有子节点的属性名和属性值
* @return 封装好的Properties对象
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
toString(builder, 0);
return builder.toString();
}
private void toString(StringBuilder builder, int level) {
builder.append("<");
builder.append(name);
for (Map.Entry<Object, Object> entry : attributes.entrySet()) {
builder.append(" ");
builder.append(entry.getKey());
builder.append("=\"");
builder.append(entry.getValue());
builder.append("\"");
}
List<XNode> children = getChildren();
if (!children.isEmpty()) {
builder.append(">\n");
for (XNode child : children) {
indent(builder, level + 1);
child.toString(builder, level + 1);
}
indent(builder, level);
builder.append("</");
builder.append(name);
builder.append(">");
} else if (body != null) {
builder.append(">");
builder.append(body);
builder.append("</");
builder.append(name);
builder.append(">");
} else {
builder.append("/>");
indent(builder, level);
}
builder.append("\n");
}
private void indent(StringBuilder builder, int level) {
for (int i = 0; i < level; i++) {
builder.append(" ");
}
}
/**
* 解析节点属性键值对,并将其放入Properties对象中
* @param n 被解析的节点
* @return Properties
*/
private Properties parseAttributes(Node n) {
Properties attributes = new Properties();
NamedNodeMap attributeNodes = n.getAttributes(); // mark 获取所有包含节点属性的NamedNodeMap对象
if (attributeNodes != null) {
// mark 遍历NamedNodeMap,将属性名和属性值存放在Properties对象中
for (int i = 0; i < attributeNodes.getLength(); i++) {
Node attribute = attributeNodes.item(i);
String value = PropertyParser.parse(attribute.getNodeValue(), variables);
attributes.put(attribute.getNodeName(), value);
}
}
return attributes;
}
/**
* 解析节点内容
* @param node 节点
* @return data 返回文本节点内容。如果节点还有子节点,那么返回第一个属于文本节点的子节点的内容。
*/
private String parseBody(Node node) {
String data = getBodyData(node); // mark 获取节点的文本内容
if (data == null) { // mark 如果是非文本节点,说明是有子节点嵌套
NodeList children = node.getChildNodes(); // mark 获取该节点下所有子节点
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
data = getBodyData(child); // mark 遍历解析节点的内容
if (data != null) {
break; // mark 获取到第一个文本子节点的文本内容,结束循环
} // mark 例:<A><B att="val"></B><C>cbody</C><A/>
} // mark 当获取到第二个子节点C的内容`cbody`时会跳出循环并返回
}
return data; // mark 只有文本节点的内容才会返回字符串。比如 <A>aaa</A> => aaa
}
private String getBodyData(Node child) {
// mark 传入节点的类型是CDATA节点或者是文本节点,即只处理文本类型的节点
// mark CDATA是未解析字符数据,即不应该由XML解析器解析的文本数据,比如"<"和"&"
if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) {
String data = ((CharacterData) child).getData(); // mark 获取节点中的值。比如 ${database.driver} => database.driver
return PropertyParser.parse(data, variables); // mark 返回带占位符的变量的值。比如将 database.driver => com.mysql.cj.jdbc.Driver
}
return null;
}
}
③ XPathParser
XPathParser类封装了Document、EntityResolver 和XPath等对象,然后提供了一系列的解析XML的方法。
Document、EntityResolver、XPath、validation、variables属性
XPathParser类一共定义了5个属性字段,分别是:
Document
:Document对象。代表整个XML文档,是一个文档树的根,可以为我们提供对文档数据的初始的访问入口。EntityResolver
:加载本地的DTD文件。如果解析mybatis-config.xml 配置文件,默认联网加载http://mybatis.org/dtd/mybatis-3- config.dtd
这个DTD 文档,当网络比较慢时会导致验证过程缓慢。在实践中往往会提前设置EntityResolver 接口对象加载本地的DTD 文件,从而避免联网加载DTD文件。XPath
:XPath对象。XPath 是一种为查询XML 文档而设计的语言,它可以与DOM 解析方式配合使用,实现对XML 文档的解析。validation
:是否开启验证标记。该标记表示设置解析器在解析文档的时候是否校验文档,在创建DocumentBuilderFactory实例对象时进行设置。variables
:配置参数集合。对应配置文件中节点下定义的键值对集合,包括通过url或者resource读取的键值对的集合。
// mark 封装了Document、EntityResolver、XPath等对象,提供了一系列解析XML的方法
public class XPathParser {
private final Document document; // mark Document对象
private boolean validation; // mark 是否开启验证
private EntityResolver entityResolver;// mark 用于加载本地的DTD文件
private Properties variables;// mark 对应配置文件中的<properties>标签定义的键值对集合
private XPath xpath; // mark XPath对象
// 很多构造方法
// 基本都很类似,主要分为四个部分Document、InputStream、Reader、String
// 内部都是调用`commonConstructor`方法设置相关的属性和`createDocument`方法为该xml文件创建一个Document对象
...
// 很多evlXXX()方法
// 用于获取Document对象中的元素或者节点
// evalXXX元素:根据表达式的值获取常用的基本类型的元素的值,其中会基于`variables`调用`PropertyParser`的`parse`方法替换掉其中的动态值(如果存在),这就是MyBatis如何替换掉XML中的动态值实现的方式
// evalXXX节点:根据表达式获取到的`org.w3c.dom.Node`节点对象,将其封装成自己定义的XNode对象,主要为了方便动态值的替换
...
private Object evaluate(String expression, Object root, QName returnType) {
try {
// mark 计算指定上下文中XPath表达式并返回指定类型的结果
// mark 如果 returnType 不是 XPathConstants(NUMBER、STRING、BOOLEAN、NODE 或 NODESET) 中定义的某种类型,则抛出 IllegalArgumentException。
// mark 如果item为null值,则将使用一个空文档作为上下文。
// mark 如果 expression 或 returnType 为 null,则抛出 NullPointerException。
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
/**
* 根据输入源创建Document对象
* @param inputSource 输入源
* @return Document 文档树对象
* (1) 创建DocumentBuilderFactory对象,并设置相关参数。
* `DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();`
* (2) 创建DocumentBuilder对象,并设置相关参数。
* `DocumentBuilder builder = factory.newDocumentBuilder();`
* (3) 解析Document对象。
* `builder.parse(inputSource);
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
// mark 创建DocumentBuilderFactory实例对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setValidating(validation); // mark 是否启动DTD验证
factory.setNamespaceAware(false); // mark 是否支持XML名称空间
factory.setIgnoringComments(true); // mark 解析器是否忽视注解
factory.setIgnoringElementContentWhitespace(false); // mark 设置是否删除元素内容中的空格
factory.setCoalescing(false); // mark 指定由此代码生成的解析器将把CDATA节点转换为Text节点,并将其附加到相邻的Text节点。
factory.setExpandEntityReferences(true); // mark 指定由此代码生成的解析器将扩展实体引用节点
DocumentBuilder builder = factory.newDocumentBuilder(); // mark 创建DocumentBuilder实例对象
builder.setEntityResolver(entityResolver); // mark 指定使用EntityResolver解析要解析的XML文档中存在的实体。如果为null,则使用默认的实现。
// mark 指定解析器要使用的ErrorHandler。如果设置为null,则使用底层的默认实现。
builder.setErrorHandler(new ErrorHandler() {
@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 {
// NOP
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
/**
* 构造器通用代码块,用于初始化用于初始化validation、entityResolver、variables、xpath等属性字段。其中,validation、entityResolver、variables三个参数通过参数传递过来;xpath属性是通过XPathFactory创建。
* @param validation
* @param variables
* @param entityResolver
*/
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance(); // mark 通过XPathFactory创建
this.xpath = factory.newXPath();
}
}
④ GenericTokenParser
MyBatis通用标记解析器,主要完成对xml属性中的占位符进行解析。
例如${name} -> name
,然后通过TokenHandler
实现的具体方法进行对象的解析处理。
public class GenericTokenParser {
private final String openToken; // 开始标记符
private final String closeToken; // 结束标记符
private final TokenHandler handler; // 标记处理接口,具体操作取决于接口的实现方法
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
// 文本解析方法
public String parse(String text) {
if (text == null || text.isEmpty()) {
return ""; // 空值判断
}
// 获取开始标记在文本中的位置
int start = text.indexOf(openToken);
if (start == -1) {
return text; // 位置索引为-1,说明不存在该开始标记标记符,直接返回文本
}
char[] src = text.toCharArray(); // 将文本字符串转化为字符数组
int offset = 0; // 偏移量
final StringBuilder builder = new StringBuilder(); // 用于拼接解析后的字符串
StringBuilder expression = null;
do {
if (start > 0 && src[start - 1] == '\\') {
// 如果开始标记之前被转义字符转义了,则删除转义字符
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length(); // 重新计算偏移量
} else {
// 已经找到开始标记,寻找结束标记
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset); // 结束标记符的索引值
while (end > -1) {
if ((end <= offset) || (src[end - 1] != '\\')) {
// 判断是否有转义字符,有就移除转义字符
expression.append(src, offset, end - offset);
break;
}
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length(); // 重新计算偏移量
end = text.indexOf(closeToken, offset); // 重新计算结束标识符索引值
}
if (end == -1) {
// 如果没有找到结束标记符
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 找到结束标记符,对该标记符进行值的替换
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset); // 继续计算下一组标记符
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现。该功能主要实现的效果是,通过解析获取到代码片段中占位符对应的变量,然后在57行通过真正的处理器TokenHandler获取该变量对应的值,然后把代码片段中的占位符替换成需要的值,比如:如果是Mybatis配置文件中的${xxx},这个时候就会直接使用xxx变量值进行直接替换;如果是SQL代码片段中的占位符#{aaa},这个时候就会把占位符整体替换成“?”,然后构成一个需要传递参数的SQL语句。
下面具体分析一下parse()方法:
第一步:其中,方法的参数text其实一般是SQL脚本字符串或者Mybatis配置文件中的占位符。上述代码从14-16行,首先验证参数问题,如果是null,就返回空字符串。否则继续执行,下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
第二步:把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码。
第三步:从27-30行,判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理。
第四步:当存在openToken且前面不包含转义字符,这个时候就可以认为这是一个占位符的开始标记。从33-34行,主要是重置expression变量,避免空指针或者老数据干扰。
第五步:第37行代码主要是把标记前的字符串存储到builder中,第39行diamante主要是重新修正偏移量offset,即偏移量为上一次的偏移量+开始标签的长度。第40行主要是判断结束标签在片段中从偏移位置起第一次出现的位置。
第六步:判断end是否大于-1,即是否存在结束标记,如果存在(大于-1),则继续判断结束标记前是否有转义字符,如果存在转义字符即认为该结束标记字符串不作为结束标记使用,就需要把从偏移量到当前位置的字符串作为表达式的一部分进行存储,并重新定义偏移量,然后继续寻找结束标记,即47、48、49行所实现效果,如果不存在转义字符就认为该结束标记字符串就是结束标记,就需要把偏移量到这个字符串中间的子串作为表达式进行处理,并直接跳出循环进行下一步处理,即44、45代码。
第七步:从51到59行代码,主要是构建需要返回的数据,并重新赋值偏移量offset,为解析下一个占位符做准备。其中else中需要根据具体处理器,进行返回指定的数据。
第七步:第61行代码是重置start开始位置,进行下个参数的解析。
第八步:从63到66行,主要是处理需要返回的结果字符串。
⑤ PropertyParser
PropertyParser主要是用于解析配置文件中<properties>
元素。
public class PropertyParser {
/**
* 定义属性时的默认前缀
*/
private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
/**
* The special property key that indicate whether enable a default value on placeholder.
* 指定是否在占位符上启用默认值的特殊属性键(是否开启默认值功能,默认不开启)
* <p>
* The default value is {@code false} (indicate disable a default value on placeholder) If you specify the
* {@code true}, you can specify key and default value on placeholder (e.g. {@code ${db.username:postgres}}).
* </p>
* 默认值是false(表示禁用占位符上的默认值)。如果您指定为true,可以指定占位符上的键和默认值。
* 例如 ${db.username:postgres},如果db.username为空,则默认值为postgres。
*
* @since 3.4.2
*/
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
/**
* The special property key that specify a separator for key and default value on placeholder.
* 特殊属性值,为键指定分隔符,并在占位符上指定默认值。(是否修改默认值的分隔符,默认是 : )
* <p>
* The default separator is {@code ":"}. 默认的分隔符是 :
* </p>
*
* @since 3.4.2
*/
public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";
/**
* 是否开启默认值功能的默认值。
*/
private static final String ENABLE_DEFAULT_VALUE = "false";
/**
* 默认分隔符。
*/
private static final String DEFAULT_VALUE_SEPARATOR = ":";
private PropertyParser() {
// Prevent Instantiation mark 私有构造方法,禁止实例化,该类主要是一个工具类,所有方法都是静态方法,所以不需要实例化。
}
/**
* 解析,创建VariableTokenHandler对象和GenericTokenParser对象,然后解析替换其中的动态值
* @param string 需要解析的代码片段
* @param variables 需要填充的参数集合
* @return
* (1) 创建VariableTokenHandler对象
* (2) 创建GenericTokenParser对象
* (3) 执行解析方法
* 该方法主要是实现代码片段的解析处理工作。该方法定义了一个处理代码片段的处理器,然后交给GenericTokenParser实例对象,
* 完成最终的解析工作。
* (1)主要是初始化一个标记处理器VariableTokenHandler(内部类),完成真正的处理工作。
* (2)主要是初始化一个GenericTokenParser实例,并设置开始标记 "${",结束标记 “}”,处理器为VariableTokenHandler实例。
* (3)主要是通过调用GenericTokenParser实例对象的parse()方法实现代码片段的解析工作。
*/
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
/**
* 字段构造器
*/
private static class VariableTokenHandler implements TokenHandler {
private final Properties variables; // mark 参数集合
private final boolean enableDefaultValue; // mark 是否开启占位符默认值
private final String defaultValueSeparator; // mark 定义占位默认值的分隔符
private VariableTokenHandler(Properties variables) {
this.variables = variables;
this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
}
private String getPropertyValue(String key, String defaultValue) {
return variables == null ? defaultValue : variables.getProperty(key, defaultValue);
}
/**
* 占位符的参数处理
* @param content 要处理的代码片段
* @return
* 该方法主要用于占位符的参数处理。首先判断variables变量是否有值,如果为null,则返回 "${" + content + "}"字符串,
* 否则,继续执行;然后判断是否开启了占位符默认值的功能;如果开启,就判断是否有默认值,
* 如果有默认值就解析成对应的key和默认值defaultValue,然后返回key对应的参数值;
* 如果没有开启默认值功能,就直接从variables获取key对应的参数值并返回。
*/
@Override
public String handleToken(String content) {
if (variables != null) {
String key = content;
if (enableDefaultValue) { // mark 是否开启占位符默认值
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) { // mark 判断是否有默认值
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
// mark 解析默认值,并返回key对应的参数
return variables.getProperty(key, defaultValue);
}
}
if (variables.containsKey(key)) {
// mark 如果没有开启默认值功能,则直接获取参数值并返回
return variables.getProperty(key);
}
}
// mark 如果variables变量为null,说明没有动态替换的值,直接返回 ${content}
return "${" + content + "}";
}
}
}