引言
在上文,我们发现直接使用 DOM库去解析XML 配置文件,非常复杂,也很不方便,需要编写大量的重复代码来处理 XML 文件的读取和解析,代码可读性以及可维护性相当差,使用起来非常不灵活。
因此,文本将在原先代码的基础上,引入XNode和XPathParser类,让 MyBatis 的配置文件解析更加简洁、灵活和高效。
XNode和XPathParser功能说明
XNode
XNode :用于封装 XML 节点,提供对 XML 节点的访问和操作能力。XNode 的设计主要是为了让开发者能够更方便地处理 XML 文件中的节点数据,而不需要直接使用 DOM 的底层 API。
XPathParser
XPathParser :用于解析 XML 文档并提供便捷的 XML 节点访问机制的类。内部使用XPath 来定位 XML 文档中的节点,使得开发者可以通过简洁的 XPath 表达式来访问 XML 节点。
XNode类实现
XNode 类为 MyBatis 提供了一种简洁的方式处理 XML 文件中的节点信息。通过封装 Node 对象,并提供一系列方法来解析节点的属性、文本内容以及子节点,XNode 类使得 XML 文件的解析变得更加简单和高效。
以下是我们当前版本设计的XNode 源码:
package org.apache.ibatis.parsing;
import org.w3c.dom.CharacterData;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* XML 节点的类封装
*
* @author crazy coder
* @since 2024/9/27
**/
public class XNode {
private final Node node;
private final XPathParser xpathParser;
private final Properties attributes;
private final String body;
// 构造函数
public XNode(XPathParser xpathParser, Node node) {
this.xpathParser = xpathParser;
this.node = node;
this.attributes = parseAttributes(node);
this.body = parseBody(node);
}
// 私有方法用于解析节点的属性
private Properties parseAttributes(Node n) {
Properties attributes = new Properties();
NamedNodeMap attributeNodes = n.getAttributes();
if (attributeNodes != null) {
for (int i = 0; i < attributeNodes.getLength(); i++) {
Node attribute = attributeNodes.item(i);
attributes.put(attribute.getNodeName(), attribute.getNodeValue());
}
}
return attributes;
}
// 私有方法用于解析节点的文本内容
private String parseBody(Node node) {
String data = getBodyData(node);
if (data == null) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
data = getBodyData(child);
if (data != null) {
break;
}
}
}
return data;
}
// 辅助方法用于获取节点的文本内容
private String getBodyData(Node child) {
if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) {
return ((CharacterData) child).getData();
}
return null;
}
// 公开方法用于评估 XPath 表达式并返回一个 XNode
public XNode evalNode(String expression) {
return xpathParser.evalNode(node, expression);
}
// 公开方法用于获取当前节点的所有子节点
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));
}
}
}
return children;
}
// 公开方法用于获取节点的文本内容
public String getBody() {
return body;
}
// 公开方法用于获取节点的属性
public Properties getAttributes() {
return attributes;
}
}
XNode 类封装了一个 Node 对象,并提供了多种方法来访问和操作这个节点。这个类主要用于解析 XML 文件中的各个节点,并提供了方便的方式来获取节点的属性、文本内容以及子节点等信息,下面是对这个类的主要功能
- 构造函数:
- 初始化 XNode 对象,传入 XPathParser 和 Node 对象。
- 解析节点的属性和文本内容。
- 属性解析:
- parseAttributes 方法用于解析节点的属性信息,并将其存储在 Properties 对象中。
- 文本内容解析:
- parseBody 方法用于解析节点的文本内容。
- getBodyData 辅助方法用于获取具体的文本或 CDATA 内容。
- 节点评估:
- evalNode 方法用于评估 XPath 表达式,并返回一个 XNode 对象。
- 子节点获取:
- getChildren 方法用于获取当前节点的所有子节点,并返回一个 XNode 对象列表。
- 获取文本内容和属性:
- getBody 方法用于获取节点的文本内容。
- getAttributes 方法用于获取节点的属性。
XNode使用场景
XNode 类通常用于解析 MyBatis 配置文件中的节点信息,例如从 元素中提取 SQL 映射信息。它可以方便地处理节点的属性、文本内容以及子节点,从而简化 XML 文件的解析逻辑。
XPathParser类实现
XPathParser 类是一个用于解析 XML 文档并提供便捷的 XML 节点访问机制的类。它利用了 Java 标准库中的 javax.xml.parsers 和 javax.xml.xpath 包来解析 XML 文档,并通过 XPath 表达式来定位和访问 XML 节点。
package org.apache.ibatis.parsing;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.*;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.Reader;
/**
* 用于解析 XML 文档并提供便捷的 XML 节点访问机制的类
*
* @author crazy coder
* @since 2024/9/27
**/
public class XPathParser {
private final Document document;
private XPath xpath;
public XPathParser(Reader reader) {
this.xpath = XPathFactory.newInstance().newXPath();
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
this.document = builder.parse(new InputSource(reader));
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
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;
}
return new XNode(this, node);
}
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
}
主要功能
- 构造函数:
- 初始化 XPathParser 对象,并解析传入的 Reader 中的 XML 文档。
- XPath 表达式评估:
- evalNode 方法用于评估 XPath 表达式,并返回一个 XNode 对象。
- evaluate 方法是私有的,用于执行 XPath 表达式的评估,并处理可能抛出的异常。
使用场景
XPathParser 类可以用于解析 MyBatis 配置文件或其他 XML 文件,并从中提取特定的节点信息。例如,在 MyBatis 中,可以使用 XPathParser 来解析 文件,并获取 、、 和 等映射节点。
基于XNode和XPathParser重写SqlSession类
原先的SqlSession直接使用原生的DOM库操作XML,相当的烦琐费事,因此我们引入设计了XNode和XPathParser类,以下代码是基于XNode和XPathParser类改造后的SqlSession类:
package org.apache.ibatis.session;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.*;
import java.util.*;
/**
* Sql会话
*
* @author crazy coder
* @since 2024/9/27
**/
public class SqlSession {
public String selectOne(String statement, Integer param) throws IOException {
final String configResource = "MapperConfig.xml";
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(configResource);
Reader reader = new InputStreamReader(in);
// 读取XML配置文件
XPathParser xPathParser = new XPathParser(reader);
XNode configNode = xPathParser.evalNode("/configuration");
// 解析XML配置信息 - 数据源
XNode dataSourceNode = configNode.evalNode("dataSource");
Properties datasourceProperties = new Properties();
for (XNode pxNode : dataSourceNode.getChildren()) {
datasourceProperties.put(pxNode.getAttributes().get("name"), pxNode.getAttributes().get("value"));
}
// 驱动
String driver = datasourceProperties.getProperty("driver");
// 数据库连接 URL
String url = datasourceProperties.getProperty("url");
// 数据库用户名
String username = datasourceProperties.getProperty("username");
// 数据库密码
String password = datasourceProperties.getProperty("password");
// 读取Mapper配置文件
List<String> resourceMapperList = new ArrayList<>();
XNode mappersNode = configNode.evalNode("mappers");
for (XNode pxNode : mappersNode.getChildren()) {
resourceMapperList.add(pxNode.getAttributes().get("resource").toString());
}
// 解析Mapper
Map<String, String> statementMap = new HashMap<>();
for (String mapperResource : resourceMapperList) {
try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) {
Reader mapperReader = new InputStreamReader(inputStream);
XPathParser mapperXPathParser = new XPathParser(mapperReader);
XNode mapperNode = mapperXPathParser.evalNode("/mapper");
String namespace = mapperNode.getAttributes().getProperty("namespace");
XNode selectNode = mapperNode.evalNode("select");
String id = selectNode.getAttributes().getProperty("id");
String sql = selectNode.getBody();
statementMap.put(namespace + "." + id, sql);
}
}
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 加载 MySQL JDBC 驱动
Class.forName(driver);
// 获取数据库连接
conn = DriverManager.getConnection(url, username, password);
// 准备 SQL 语句
String sql = statementMap.get(statement);
// 创建预编译语句
pstmt = conn.prepareStatement(sql);
// 设置参数
pstmt.setLong(1, param);
// 执行 SQL 查询操作
rs = pstmt.executeQuery();
// 处理结果集
StringBuilder result = new StringBuilder();
while (rs.next()) {
result.append("id: ").append(rs.getInt("id"))
.append(", username: ").append(rs.getString("username"))
.append(", email: ").append(rs.getString("email"));
}
return result.toString();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
in.close();
}
return "";
}
}
当前版本SqlSession代码相对于上个版本,主要变更点如下
- 读取配置文件:
加载 MapperConfig.xml 配置文件,并使用 XPathParser 的 evalNode 方法找到 /configuration 节点。 - 解析数据源信息:
调用XNode的getChildren方法,获取数据源所有子节点(驱动、URL、用户名和密码),通过调用XNode的getAttributes方法获取属性值。 - 读取 Mapper 文件:
加载配置文件中指定的 Mapper 文件,并解析 Mapper 文件中的 SQL 映射信息,通过XNode的getBody方法获取SQL。
整体项目结构
总结
本文我们实现了以下功能:
- XNode类实现:通过封装 Node 对象,并提供一系列方法来解析节点的属性、文本内容以及子节点,XNode 类使得 XML 文件的解析变得更加简单和高效。
- XPathParser类实现:通过封装 XML 文档的解析和 XPath 表达式的评估,XPathParser 类使得 XML 文件的解析变得更加简单和高效。