首先,说明一些自己学习过程中产生的问题:
- XML 文档解析问题
- 怎样处理参数以及返回结果
最初,看完自定义持久层框架部分后,我发现了课程里面的简单的框架雏形与 MyBatis 源码之中有很多出入部分,于是我在思考,为什么 MyBatis 要这样做?于是我决定不只是简单地写一个自定义持久层框架,而是参照着 MyBatis 源代码实现一个简版的 MyBatis 框架。
-
XML 配置文件解析问题:
翻阅 MyBatis 源码,我们可以发现,MyBatis 没有使用课程中的 Dom4J 技术,而是使用 DOM 搭配 XPath 来完成对 XML 配置文件的解析,那么为什么要这样做?
在 Java 中,一共有这样几种解析方式:
-
DOM
优点:
- 形成了树形结构,直观好理解,代码更易编写
- 解析过程中树结构保存在内存中,方便修改
缺点:
- 当 XML 文件较大时,对内存耗费比较大,容易影响解析性能并造成内存溢出
当然,在 MyBatis 中直接将解析的内容封装成了实体类 Configuration 等等。
-
SAX
优点:
- 采用事件驱动模式,对内存耗费比较小
- 适用于只需要处理 XML 中数据
缺点:
- 不易编码
- 很难同时访问同一个 XML 中多出不同数据
-
JDOM
仅使用具体类而不使用接口,并且 API 大量使用了 Collections 类
-
Dom4J
JDOM 的一种智能分支,合并了许多超出基本 XML 文档表示的功能。内部使用接口和抽象基本类方法,是一个优秀的 Java XML API。具有性能优异、灵活性好、功能强大和极端易用的特点,并且是一款开源的插件。
因为这款插件的本质就是对 JDOM 的封装,而且因为其功能过分强大,无可避免的会有一些冗余,而 MyBatis 本身是一款轻量级的框架,自然不能使用这种方式。仅代表个人看法。
那么 MyBatis 中是如何解析 XML 的呢?下面参照着我的代码来给大家进行讲解。
-
XNode 标签节点类,对每一个标签内容进行解析,并封装成 XNode 对象。
public class XNode { // 当前解析的标签 Node 对象 private final Node node; // 当前解析标签名称 private final String name; // 当前解析标签中的内容. <mapper>...</mapper> private final String body; // 当前标签对应的属性集合. 例如: <mapper namespace = "..."></mapper> 中的 namespace = "..." private final Properties attributes; // XPath 标签解析器 private final XPathParser xpathParser; // 类中的所有成员变量皆为常量, 不可以进行修改, 只能通过构造方法进行初始化 public XNode(Node node, XPathParser xpathParser) { this.node = node; this.name = node.getNodeName(); this.body = this.parseBody(node); this.attributes = this.parseAttributes(node); this.xpathParser = xpathParser; } public Node getNode() { return node; } // 解析标签中的内容 private String parseBody(Node node) { String data = this.getBodyData(node); if (data == null) { NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); ++i) { Node child = children.item(i); data = this.getBodyData(child); if (data != null) { break; } } } return data; } private String getBodyData(Node child) { // 判断标签类型, 只有 Text node 以及 CDATASection 类型才需要进行解析 if (child.getNodeType() != 4 && child.getNodeType() != 3) { return null; } else { return ((CharacterData) child).getData(); } } // 解析标签的属性, 并存放到 Properties 对象中 private Properties parseAttributes(Node node) { Properties attributes = new Properties(); NamedNodeMap attributeNodes = node.getAttributes(); if (attributeNodes != null) { for (int i = 0; i < attributeNodes.getLength(); ++i) { Node attribute = attributeNodes.item(i); String value = attribute.getNodeValue(); attributes.put(attribute.getNodeName(), value); } } return attributes; } // 根据 XPath 解析标签, 将解析结果封装成 XNode 对象 public XNode evalNode(String expression) { return this.xpathParser.evalNode(this.node, expression); } // 解析的标签存在多个, 返回 XNode 对象集合 public List<XNode> evalNodes(String expression) { return this.xpathParser.evalNodes(this.node, expression); } public String getStringBody() { return this.body; } // 根据属性名称获取属性值 public String getStringAttribute(String name) { return this.attributes.getProperty(name); } // 获取当前标签下的所有子标签的集合 public List<XNode> getChildren() { NodeList nodeList = this.node.getChildNodes(); if (nodeList != null) { int length = nodeList.getLength(); List<XNode> children = new ArrayList<>(length); for (int i = 0; i < length; i++) { Node node = nodeList.item(i); if (node.getNodeType() == 1) { children.add(new XNode(node, this.xpathParser)); } } return children; } return null; } // 获取当前标签下的所有子标签的属性, 例如解析 dataSource 标签时采取这种方式 public Properties getChildrenAsProperties() { Properties properties = new Properties(); List<XNode> children = this.getChildren(); if (children != null) { for (XNode node : children) { String name = node.getStringAttribute("name"); String value = node.getStringAttribute("value"); if (name != null && value != null) { properties.setProperty(name, value); } } } return properties; } }
-
XPathParser XPath 解析器,负责解析 XML 文档中的节点,并将解析结果封装成 XNode 对象。
package cn.worstone.bean; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; 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.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.InputStream; import java.util.ArrayList; import java.util.List; public class XPathParser { // 需要解析的 XML 文档对应的 Document 对象 private final Document document; // XPath 对象 private XPath xPath; // 根据 XML 文件流构建 XPath 解析器 public XPathParser(InputStream inputStream) { this.document = this.createDocument(inputStream); XPathFactory factory = XPathFactory.newInstance(); this.xPath = factory.newXPath(); } public Object evaluate(String expression, QName returnType) { return evaluate(expression, this.document, returnType); } // 根据 XPath 表达式、解析的对象以及返回结果类型解析标签 // 默认不指定解析对象即为当前的 document 对象 // 根据不同的返回结果分别进行了封装 public Object evaluate(String expression, Object item, QName returnType) { try { return this.xPath.evaluate(expression, item, returnType); } catch (XPathExpressionException e) { throw new RuntimeException("Error evaluating XPath. Cause: " + e); } } public XNode evalNode(String expression) { return this.evalNode(this.document, expression); } // XPathConstants.NODE public XNode evalNode(Object item, String expression) { Node node = (Node) this.evaluate(expression, item, XPathConstants.NODE); return node == null ? null : new XNode(node, this); } public List<XNode> evalNodes(String expression) { return this.evalNodes(this.document, expression); } // XPathConstants.NODESET public List<XNode> evalNodes(Object item, String expression) { NodeList nodeList = (NodeList) this.evaluate(expression, item, XPathConstants.NODESET); List<XNode> nodes = new ArrayList<>(); if (nodeList != null) { for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node != null) { nodes.add(new XNode(node, this)); } } } return nodes; } // 根据不同设置以及输入字节流创建 Document 文档对象 private Document createDocument(InputStream inputStream) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(false); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputStream); } catch (Exception e) { throw new RuntimeException("Error creating document instance. Cause: " + e); } } }
当然 MyBatis 中内部实现不可能这样简单,例如 XNode 解析标签内容的时候,MyBatis 其实是使用了标记处理器对内容中的
${}
占位符进行处理。当源码翻阅到这个位置的时候,我再一次产生了一个问题:SQL 语句中的
#{}
占位符是什么时候解析的呢?这个问题后面会找到答案。 -
XMLConfigBuilder 这个文件才是 MyBatis 核心配置文件的解析入口。
package cn.worstone.builder.xml; import cn.worstone.bean.Configuration; import cn.worstone.bean.XNode; import cn.worstone.bean.XPathParser; import cn.worstone.io.Resources; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; import java.io.InputStream; import java.util.List; import java.util.Properties; public class XMLConfigBuilder { // 标记当前配置文件是否解析 private boolean parsed; // XPath 解析器 private final XPathParser parser; // 封装配置信息的实体类 private Configuration configuration; public XMLConfigBuilder(InputStream inputStream) { this.parsed = false; this.parser = new XPathParser(inputStream); this.configuration = new Configuration(); } public Configuration parse() { if (this.parsed) { throw new RuntimeException("Each XMLConfigBuilder can only be used once."); } else { this.parsed = true; // 解析配置文件的根标签 configuration this.parseConfiguration(this.parser.evalNode("/configuration")); } return this.configuration; } public void parseConfiguration(XNode root) { // 因为只是 MyBatis 的简单实现, 这里仅需要实现数据源以及映射器的解析 DataSource dataSource = dataSourceElement(root.evalNode("dataSource")); this.configuration.setDataSource(dataSource); mapperElement(root.evalNode("mappers")); } // 对数据源 dataSource 标签进行解析 private DataSource dataSourceElement(XNode context) { if (context != null) { // 这里就是上面提到过的, 直接获取所有 property 标签的属性 Properties properties = context.getChildrenAsProperties(); // 这里使用的是 Druid 数据库连接池, 而在 MyBatis 中, 有一个基于"池"概念简单实现的数据库连接池 POOLED, 其内部是对用户关闭数据源操作的一个拦截 // 在MyBatis 中此处是使用连接池工厂创建连接池, 并根据解析后的数据源属性返回一个数据源信息 DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(properties.getProperty("driver")); dataSource.setUrl(properties.getProperty("url")); dataSource.setUsername(properties.getProperty("username")); dataSource.setPassword(properties.getProperty("password")); return dataSource; } else { throw new RuntimeException("Configuration declaration requires a DataSource."); } } // 对映射器 mappers 标签进行解析 private void mapperElement(XNode context) { if (context != null) { List<XNode> children = context.getChildren(); for (XNode node : children) { String resource = node.getStringAttribute("resource"); InputStream inputStream = Resources.getResourceAsStream(resource); // 构建 XMLMapperBuilder 对象对每个映射器文件进行解析 XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(inputStream, this.configuration, resource); mapperBuilder.parse(); } } } }
-
XMLMapperBuilder 映射配置文件解析
package cn.worstone.builder.xml; import cn.worstone.bean.Configuration; import cn.worstone.bean.XNode; import cn.worstone.bean.XPathParser; import java.io.InputStream; import java.util.Iterator; import java.util.List; public class XMLMapperBuilder { // 映射配置文件的命名空间 private String namespace; // 当前解析映射配置文件的资源路径 private final String resource; // XPath 解析器 private final XPathParser parser; // 封装配置信息实体类 private final Configuration configuration; public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource) { this.parser = new XPathParser(inputStream); this.resource = resource; this.configuration = configuration; } public void parse() { // 解析映射配置文件的根标签 mapper this.configurationElement(this.parser.evalNode("/mapper")); // 绑定映射配置文件 Mapper 接口, 将其添加到 Configuration 对象的 mapperRegistry 中 this.bindMapperForNamespace(); } private void configurationElement(XNode context) { // 获取当前映射配置文件的 namespace, MyBatis 中该类没有这个成员变量, 而是将其封装在 MapperBuilderAssistant 中, 用于后续使用 this.namespace = context.getStringAttribute("namespace"); // 解析 SQL 语句对应标签 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } private void buildStatementFromContext(List<XNode> nodes) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { XNode node = (XNode) iterator.next(); // 创建 XMLStatementBuilder 对象, 对 SQL 语句标签进行解析, 并封装成 MappedStatement 存放到 Configuration 对象中 XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, node, this.namespace); statementParser.parseStatementNode(); } } // 通过 namespace 获取对应 Mapper 接口, 然后创建其代理对象存放在 mapperRegistry private void bindMapperForNamespace() { if (this.namespace != null) { Class boundType = null; try { boundType = this.getClass().getClassLoader().loadClass(this.namespace); } catch (ClassNotFoundException e) { } if (boundType != null && !this.configuration.hasMapper(boundType)) { this.configuration.addMapper(boundType); } } } }
-
XMLStatementBuilder 解析映射配置文件中的 SQL 语句
package cn.worstone.builder.xml; import cn.worstone.bean.*; import cn.worstone.scripting.LanguageDriver; import cn.worstone.scripting.xml.XMLLanguageDriver; import cn.worstone.type.SqlCommandType; import java.util.Locale; public class XMLStatementBuilder { // 封装配置信息的实体类 private final Configuration configuration; // 当前解析标签的 XNode private final XNode node; // 当前标签对应的命名空间 private final String namespace; public XMLStatementBuilder(Configuration configuration, XNode node, String namespace) { this.configuration = configuration; this.node = node; this.namespace = namespace; } // 解析 SQL 语句, 将其封装成 MappedStatement 对象 public void parseStatementNode() { // 当前的 SQL 标签名称, select insert update delete String nodeName = this.node.getNode().getNodeName(); // 通过标签名称得到 SqlCommandType. 这是一个枚举类型, 封装 SQL 语句类型, 用于 Mapper 接口开发方式中调用方法的判断 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); // 获取当前标签的 id 属性 String id = this.node.getStringAttribute("id"); // 通过 id 属性以及 namespace 属性, 通过该方法生成 namespace.id 的唯一标识 String statement = this.applyCurrentNamespace(id); // 获取 resultType 属性 String resultType = this.node.getStringAttribute("resultType"); // 获取 parameterType 属性 String parameterType = this.node.getStringAttribute("parameterType"); // 通过类全限定名获取其 Class Class<?> resultTypeClass = this.resolveClass(resultType); Class<?> parameterTypeClass = this.resolveClass(parameterType); // 封装 ResultMap ResultMap resultMap = new ResultMap(id, resultTypeClass); // 语言驱动, 在 MyBatis 中包含多种语言驱动, 应对不同场景 LanguageDriver driver = new XMLLanguageDriver(); // 解析 SQL 语句, 将解析内容封装成 SQLSource SqlSource sqlSource = driver.createSqlSource(this.node); // 将参数信息封装成 ParameterMap 对象 ParameterMap parameterMap = new ParameterMap(id, parameterTypeClass, sqlSource.getParameterMappings()); parameterMap.build(); // 将封装好的 MappedStatement 对象存放到 Configuration 对象中 this.configuration.getMappedStatements().put(statement, new MappedStatement(id, resultMap, parameterMap, sqlSource, this.configuration, sqlCommandType)); } // 通过 id 属性以及 namespace 属性, 通过该方法生成 namespace.id 的唯一标识 private String applyCurrentNamespace(String base) { if (base == null) { return null; } else { if (base.startsWith(this.namespace + ".")) { return base; } if (base.contains(".")) { throw new RuntimeException("Dots are not allowed in element names, please remove it from " + base); } } return this.namespace + "." + base; } // 通过类全路径获取其 Class private <T> Class<? extends T> resolveClass(String alias) { if (alias == null) { return null; } else { try { return (Class<? extends T>) Class.forName(alias); } catch (Exception e) { throw new RuntimeException("Error resolving class. Cause: " + e); } } } }
-
XMLLanguageDriver SQL 语句处理驱动
package cn.worstone.scripting.xml; import cn.worstone.bean.BoundSql; import cn.worstone.bean.MappedStatement; import cn.worstone.bean.SqlSource; import cn.worstone.bean.XNode; import cn.worstone.handler.DefaultParameterHandler; import cn.worstone.handler.ParameterHandler; import cn.worstone.scripting.LanguageDriver; public class XMLLanguageDriver implements LanguageDriver { public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); } // 解析 SQL 语句 @Override public SqlSource createSqlSource(XNode node) { // 创建 XMLScriptBuilder 对象, 解析 SQL 语句 XMLScriptBuilder builder = new XMLScriptBuilder(node); return builder.parseScriptNode(); } }
在这里有些同学可能会问,为什么要这么麻烦,不就是解析 SQL 语句嘛?但是我们要知道,在 MyBatis 中有许多种方式编写 SQL 语句,直接在 XML 中编写或者使用注解方式开发,其次在 MyBatis 中包含许多动态 SQL 语句的实现方式,方便我们使用,这些种种被 MyBatis 拆分然后实现,因为要符合 “高内聚、低耦合”的设计原则,所以 MyBatis 框架中拆分得非常细致,并且异常处理机制十分周全。
-
XMLScriptBuilder 解析 SQL 语句,封装内容成 SqlSource,这里与课程里面的不太一样,课程里面直接封装成了 BoundSql,但是在 MyBatis 中是封装成了 SqlSource,BoundSql 只是负责将 SQL 信息在这个地方传递使用。
package cn.worstone.scripting.xml; import cn.worstone.bean.SqlSource; import cn.worstone.bean.XNode; import cn.worstone.builder.SqlSourceBuilder; public class XMLScriptBuilder { private final XNode context; public XMLScriptBuilder(XNode context) { this.context = context; } // 通过SqlSourceBuilder 对象解析 SQL 语句, 封装到 SqlSource 中并返回 public SqlSource parseScriptNode() { SqlSourceBuilder builder = new SqlSourceBuilder(); return builder.parse(this.context.getStringBody()); } }
-
SqlSourceBuilder 通过通用标识符解析器对 SQL 语句进行解析
package cn.worstone.builder; import cn.worstone.bean.SqlSource; import cn.worstone.parsing.GenericTokenParser; import cn.worstone.parsing.ParameterMappingTokenHandler; public class SqlSourceBuilder { public SqlSource parse(String originalSql) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new SqlSource(sql, handler.getParameterMappings()); } }
MyBatis 就是在这个地方进行
#{}
占位符进行解析,将占位符替换成?
并将其中类型名称存放到 ParameterMappding 集合中,然后封装到 SqlSource 中。 -
SqlSource 封装了解析后的 SQL 语句,其中的占位符已经替换成了
?
,以及解析出来的属性名称集合package cn.worstone.bean; import java.util.List; public class SqlSource { // 解析后 SQL 语句 private final String sql; // 解析出来的属性名称集合, 占位符中的参数名称 private final List<ParameterMapping> parameterMappings; public SqlSource(String sql, List<ParameterMapping> parameterMappings) { this.sql = sql; this.parameterMappings = parameterMappings; } // 通过传入参数获取 BoundSql public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(this.sql, parameterObject, this.parameterMappings); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } }
至此,解析 XML 配置文件讲解完毕。
我相信大伙一定对 MyBatis 中使用 Map 传递参数并不陌生,这种方式堪称万能,真的是非常好用,但为什么可以这样用呢?在教学视频中,子慕老师是使用两个查询作为例子,那么在增删改操作中,我们是否可以同样支持 Map 以及 String 类型参数直接传入呢?接下来,我们先带着这个问题,探究一下数据库操作的执行流程。
这里先说明一下群里小伙伴的一个问题,关于 MyBatis 自动提交这部分源码。
-
DefaultSqlSessionFactory 是 SqlSessionFactory 的默认实现,就是用来创建 SqlSession 对象的工厂
package cn.worstone.session.defaults; import cn.worstone.bean.Configuration; import cn.worstone.executor.Executor; import cn.worstone.session.SqlSession; import cn.worstone.session.SqlSessionFactory; import cn.worstone.transaction.JDBCTransaction; import cn.worstone.transaction.Transaction; public class DefaultSqlSessionFactory implements SqlSessionFactory { // 封装配置信息的实体类 private final Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } // 创建一个不自动提交的 SqlSession 对象 @Override public SqlSession openSession() { return openSessionFromDataSource(false); } // 通过设置是否自动提交属性创建一个 SqlSession 对象 @Override public SqlSession openSession(boolean autoCommit) { return openSessionFromDataSource(autoCommit); } // 从数据源打开一个会话连接 private SqlSession openSessionFromDataSource(boolean autoCommit) { Transaction transaction = null; try { // 创建一个事务对象 transaction = new JDBCTransaction(this.configuration.getDataSource(), autoCommit); // 根据事务创建一个执行器 Executor executor = this.configuration.newExecutor(transaction); // 创建一个 SqlSession 对象 return new DefaultSqlSession(this.configuration, executor); } catch (Exception e) { // 出现异常关闭事务 this.closeTransaction(transaction); throw new RuntimeException("Error opening session. Cause: " + e); } } private void closeTransaction(Transaction transaction) { try { if (transaction != null) { transaction.close(); } } catch (Exception e) { e.printStackTrace(); } } }
-
JDBCTransaction JDBC 事务
package cn.worstone.transaction; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class JDBCTransaction implements Transaction { // 配置信息 private Connection connection; // 数据源 private DataSource dataSource; // 是否自动提交 private boolean autoCommit; public JDBCTransaction(DataSource dataSource, boolean desiredAutoCommit) { this.dataSource = dataSource; this.autoCommit = desiredAutoCommit; } // 获取数据库连接 @Override public Connection getConnection() throws SQLException { if (this.connection == null) { this.openConnection(); } return this.connection; } // 提交事务 @Override public void commit() throws SQLException { this.connection.commit(); } // 回滚事务 @Override public void rollback() throws SQLException { this.connection.rollback(); } // 关闭事务 @Override public void close() throws SQLException { this.connection.close(); } // 从数据源中获取数据库连接 public void openConnection() throws SQLException { this.connection = this.dataSource.getConnection(); // 设置自动提交属性 this.setDesiredAutoCommit(this.autoCommit); } // 设置数据库连接自动提交属性 private void setDesiredAutoCommit(boolean desiredAutoCommit) { // 由于这个方法是在 openConnection 中调用, 所以无须判断当前 connection 是否为 null try { // 如果当前连接中的自动提交设置与用户设置的值不一致, 重新设置当前连接的自动提交属性 if (this.connection.getAutoCommit() != autoCommit) { this.connection.setAutoCommit(autoCommit); } } catch (Exception e) { throw new RuntimeException("Error configuring AutoCommit. Your driver may not support getAutoCommit() or setAutoCommit(). Requested setting: " + desiredAutoCommit + ". Cause: " + e); } } }
在 MyBatis 中有三种事务的实现方式,JDBC 只是其中一种,而自动提交部分的源码就在这里。
通过学习,我们都应该知道, MyBatis 中的 SqlSession 中封装了许多对数据库操作的方法,所以想要知道 SQL 语句的执行流程,我们则需要通过查阅这部分源码进行探究。
-
DefaultSqlSession SqlSession 的默认实现类,封装了对数据库的操作,以及事务相关的操作
package cn.worstone.session.defaults; import cn.worstone.bean.BoundSql; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.executor.Executor; import cn.worstone.session.SqlSession; import java.sql.SQLException; import java.util.List; public class DefaultSqlSession implements SqlSession { // 配置文件 private final Configuration configuration; // 执行器 private Executor executor; public DefaultSqlSession(Configuration configuration, Executor executor) { this.configuration = configuration; this.executor = executor; } // 查询集合, 无参 @Override public <T> List<T> selectList(String statement) { return this.selectList(statement, null); } // 查询集合, 有参 @Override public <T> List<T> selectList(String statement, Object parameter) { List<T> list; // 根据 MappedStatement 唯一标识从配置文件中获取 MappedStatement MappedStatement mappedStatement = this.configuration.getMappedStatements().get(statement); try { // 调用执行器中的 query 方法 list = this.executor.query(mappedStatement, parameter, mappedStatement.getSqlSource().getBoundSql(parameter)); } catch (Exception e) { throw new RuntimeException("Error querying database. Cause: " + e); } return list; } // 查询单个 @Override public <T> T selectOne(String statement, Object parameter) { List<T> list = this.selectList(statement, parameter); if (list.size() > 1) { throw new RuntimeException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else if (list.size() == 1) { return list.get(0); } return null; } // 添加方法 @Override public int insert(String statement, Object parameter) { return this.update(statement, parameter); } // 更新方法 @Override public int update(String statement, Object parameter) { MappedStatement mappedStatement = this.configuration.getMappedStatements().get(statement); BoundSql boundSql = mappedStatement.getBoundSql(parameter); try { return this.executor.update(mappedStatement, parameter, boundSql); } catch (Exception e) { throw new RuntimeException("Error update database. Cause: " + e); } } // 删除方法 @Override public int delete(String statement, Object parameter) { return this.update(statement, parameter); } // 事务提交 @Override public void commit() throws SQLException { this.executor.commit(); } // 事务回滚 @Override public void rollback() throws SQLException { this.executor.rollback(); } // 关闭连接 @Override public void close() { try { this.executor.close(); } catch (SQLException e) { e.printStackTrace(); } } // 获取配置文件 @Override public Configuration getConfiguration() { return this.configuration; } // 获取 Mapper @Override public <T> T getMapper(Class<T> mapperClass) { return this.configuration.getMapper(mapperClass, this); } }
因为会有需要手动控制事务的场景,所以在 SqlSession 中添加了事务操作。由于传统 Dao 的开发方式的弊端,所以 MyBatis 在 SqlSession 中添加了 Mapper 接口方法。
在 MyBatis 中,SqlSession 调用的是 Executor 方法去执行数据库操作。
-
Executor 执行器,在 MyBatis 中,一二级缓存相关实现就是在这实现的。这里用到了装饰器模式,Executor 是接口,BaseExecutor 是一个抽象类,SimpleExecutor 是默认的实现类,当开启了二级缓存,就会创建一个 CachingExecutor,其中包含一个 Executor 对象,并且额外实现了二级缓存的相关功能。由于时间关系,缓存相关功能没有实现。
package cn.worstone.executor; import cn.worstone.bean.BoundSql; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.transaction.Transaction; import java.sql.Connection; import java.sql.SQLException; import java.util.List; public abstract class BaseExecutor implements Executor { // 配置文件 protected final Configuration configuration; // 事务 protected final Transaction transaction; protected BaseExecutor(Configuration configuration, Transaction transaction) { this.configuration = configuration; this.transaction = transaction; } // 查询方法 @Override public <T> List<T> query(MappedStatement ms, Object parameter, BoundSql boundSql) { return this.queryFromDatabase(ms, parameter, boundSql); } // 更新方法 @Override public int update(MappedStatement ms, Object parameter, BoundSql boundSql) throws SQLException, NoSuchFieldException, IllegalAccessException { return this.doUpdate(ms, parameter); } // 因为查询方法有缓存的存在, 所有有两种情况: 1. 当前查询信息存在缓存中, 此时无需从数据库中查询 2. 当前查询信息在缓存中没有, 则需要从数据库中查询 private <E> List<E> queryFromDatabase(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) { List list = null; try { list = this.doQuery(mappedStatement, parameter, boundSql); } catch (Exception e) { e.printStackTrace(); } return list; } // 获取数据库连接 protected Connection getConnection() throws SQLException { return this.transaction.getConnection(); } // 真正的更新操作 protected abstract int doUpdate(MappedStatement mappedStatement, Object parameters) throws SQLException; // 真正的查询操作 protected abstract <E> List<E> doQuery(MappedStatement mappedStatement, Object parameters, BoundSql boundSql) throws SQLException; // 事务相关操作 public void commit() throws SQLException { this.transaction.commit(); } public void rollback() throws SQLException { this.transaction.rollback(); } public void close() throws SQLException { this.transaction.close(); } }
其实,事务相关操作都是在 BaseExecutor 中实现的,SqlSession 中调用的 BaseExecutor 相关方法,因为 MyBatis 中有缓存相关操作,所以抽象出一个 queryFromDatabase 方法。
package cn.worstone.executor; import cn.worstone.bean.BoundSql; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.handler.StatementHandler; import cn.worstone.transaction.Transaction; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.List; public class SimpleExecutor extends BaseExecutor { public SimpleExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } // 数据库更新操作 @Override protected int doUpdate(MappedStatement mappedStatement, Object parameter) throws SQLException { Statement statement = null; try { BoundSql boundSql = mappedStatement.getBoundSql(parameter); // 获取 Statement 处理器 StatementHandler handler = configuration.newStatementHandler(this, mappedStatement, parameter, boundSql); statement = this.prepareStatement(handler); return handler.update(statement); } finally { if (statement != null) { statement.close(); } } } // 数据库查询操作 @Override protected <E> List<E> doQuery(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) throws SQLException { Statement statement = null; try { StatementHandler handler = configuration.newStatementHandler(this, mappedStatement, parameter, boundSql); statement = this.prepareStatement(handler); return handler.query(statement); } finally { if (statement != null) { statement.close(); } } } // 获取 Statement 实例 private Statement prepareStatement(StatementHandler handler) throws SQLException { Connection connection = this.getConnection(); Statement statement = handler.prepare(connection); return statement; } }
-
StatementHandler SQL 处理器,所有的数据库操作在这实现的。
package cn.worstone.handler; import cn.worstone.bean.BoundSql; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.executor.Executor; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public abstract class BaseStatementHandler implements StatementHandler { // 配置信息 protected final Configuration configuration; // 执行器 protected final Executor executor; // 结果处理器 protected final ResultSetHandler resultSetHandler; // 参数处理器 protected final ParameterHandler parameterHandler; // SQL 映射配置信息 protected final MappedStatement mappedStatement; // SQL 语句相关信息 protected BoundSql boundSql; protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.boundSql = boundSql; this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, this.parameterHandler, boundSql); } // Statement 准备方法实现一致, 所以直接在抽象类中实现, 其中 instantiateStatement 方法实现有可能存在变化, 故没有在抽象类中实现 // 将各部分代码独立出去, 形成一个个单独的功能方法, "高内聚, 低耦合" public Statement prepare(Connection connection) { Statement statement = null; try { statement = this.instantiateStatement(connection); return statement; } catch (Exception e) { this.closeStatement(statement); throw new RuntimeException("Error preparing statement. Cause: " + e); } } // 实例化 Statement 对象 protected abstract Statement instantiateStatement(Connection connection) throws SQLException; // 关闭方法不存在什么不同, 所以直接在抽象类中实现 protected void closeStatement(Statement statement) { try { if (statement != null) { statement.close(); } } catch (SQLException e) { } } }
package cn.worstone.handler; import cn.worstone.bean.BoundSql; import cn.worstone.bean.MappedStatement; import cn.worstone.executor.Executor; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.util.List; public class PreparedStatementHandler extends BaseStatementHandler { public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, BoundSql boundSql) { super(executor, mappedStatement, parameter, boundSql); } @Override protected Statement instantiateStatement(Connection connection) throws SQLException { return connection.prepareStatement(this.boundSql.getSql()); } // 更新方法 @Override public int update(Statement statement) throws SQLException { PreparedStatement preparedStatement = (PreparedStatement) statement; // 参数处理器设置传入参数 this.parameterHandler.setParameters(preparedStatement); preparedStatement.execute(); // 因为更新操作返回结果只是 int 或者 void, 所以直接返回影响行数 return preparedStatement.getUpdateCount(); } // 查询方法 @Override public <E> List<E> query(Statement statement) throws SQLException { PreparedStatement preparedStatement = (PreparedStatement) statement; this.parameterHandler.setParameters(preparedStatement); preparedStatement.execute(); // 处理返回结果集 return this.resultSetHandler.handleResultSets(preparedStatement); } }
在 MyBatis 中默认是使用预处理方式处理 SQL 语句。关于 MyBatis 中的占位符
#{}
以及${}
,#{}
占位符底层是以 PreparedStatement 预处理的方式处理 SQL 语句,不会出现 SQL 注入问题;而${}
占位符是以字符串拼接的方式拼接参数,所以会出现 SQL 注入问题,所以使用的时候需要注意,但是可以动态设置表名。 -
ParameterHandler 传入参数处理器
package cn.worstone.handler; import cn.worstone.bean.*; import java.lang.reflect.Field; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; import java.util.Map; public class DefaultParameterHandler implements ParameterHandler { private final MappedStatement mappedStatement; private final Object parameterObject; private final BoundSql boundSql; private final Configuration configuration; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.parameterObject = parameterObject; this.boundSql = boundSql; this.configuration = mappedStatement.getConfiguration(); } @Override public Object getParameterObject() { return this.parameterObject; } @Override public void setParameters(PreparedStatement preparedStatement) throws SQLException { try { ParameterMap parameterMap = this.mappedStatement.getParameterMap(); Class parameterClass = parameterMap.getType(); List<ParameterMapping> parameterMappings = parameterMap.getParameterMappings(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); String property = parameterMapping.getProperty(); Object value; // 当前只添加了自定义实体类、Map 以及 String 类型参数处理, 后续抽象出具体的类型处理器进行处理 if (parameterClass.isAssignableFrom(Map.class)) { // Map 类型, 根据 #{} 中的属性名获取属性值 value = ((Map) this.parameterObject).get(property); } else if (parameterClass.isAssignableFrom(String.class)) { // String 类型, 参数对象即为设置参数 value = this.parameterObject; } else { // 自定义实体类, 通过 Java 反射技术获取对应的属性值 Field declaredField = parameterClass.getDeclaredField(property); declaredField.setAccessible(true); value = declaredField.get(this.parameterObject); } preparedStatement.setObject(i + 1, value); } } catch (Exception e) { throw new RuntimeException("The parameters cannot be set. Cause: " + e); } } }
当我写这段代码的时候出现了一个非常有意思的情况,我当时想到了传入参数为 Map 的情况,但是我的删除方法是使用了传入单个 String 类型的方法,但是当时我是没有对 String 类型进行处理的,所以报了错,然后查找到了这个位置,看到这块代码,大家可以明显发现缺点,类型映射部分还是不健全,而且可以单独抽象出去。
-
ResultSetHandler 返回结果集处理器
package cn.worstone.handler; import cn.worstone.bean.BoundSql; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.executor.Executor; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; public class DefaultResultSetHandler implements ResultSetHandler { private final Executor executor; private final Configuration configuration; private final MappedStatement mappedStatement; private final ParameterHandler parameterHandler; private final BoundSql boundSql; public DefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, BoundSql boundSql) { this.executor = executor; this.configuration = mappedStatement.getConfiguration(); this.mappedStatement = mappedStatement; this.parameterHandler = parameterHandler; this.boundSql = boundSql; } // MyBatis 将功能拆分得非常细致, 可以让开发人员迅速定位到报错的位置. 大致原理都是依靠 Java 中的反射技术实现的, 内省技术也是反射技术封装实现的. @Override public <E> List<E> handleResultSets(Statement statement) throws SQLException { Class resultClass = this.mappedStatement.getResultMap().getType(); ResultSet resultSet = statement.getResultSet(); try { List<E> resultList = new ArrayList<>(resultSet.getRow()); while (resultSet.next()) { ResultSetMetaData metaData = resultSet.getMetaData(); E result = (E) resultClass.getDeclaredConstructor().newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { String columnName = metaData.getColumnName(i); Object value = resultSet.getObject(columnName); PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass); Method writer = propertyDescriptor.getWriteMethod(); writer.invoke(result, value); } resultList.add(result); } return resultList; } catch (Exception e) { throw new RuntimeException("The returned result set cannot be encapsulated. Cause: " + e); } finally { if (resultSet != null) { resultSet.close(); } } } }
这两部分代码其实只是课程中的简单实现,使用 Java 内省技术实现结果集的封装。在 MyBatis 中,这部分实现复杂的多,而不是这样简单的实现。至于我为什么将其拆分开,因为当我按照课程内容实现了更新方法的时候,我发现与查询方法十分相似,代码十分冗余,所以将其中内容按照功能进行拆分。
-
Configuration 配置信息实体类
其实这个类在 MyBatis 中可以说是最核心的一个实体类,不仅存放着配置信息,而且无处不在,许多地方都是依靠这个对象传递相关配置信息,而且这个方法里面封装了很多方法,功能十分强大。生成 StatementHandler 、ParameterHandler以及ResultSetHandler实例的方法全都是在 Configuration 中,而且实现插件的
this.interceptorChain.pluginAll()
就是在此处。package cn.worstone.bean; import cn.worstone.binding.MapperRegistry; import cn.worstone.executor.Executor; import cn.worstone.executor.SimpleExecutor; import cn.worstone.handler.*; import cn.worstone.scripting.LanguageDriver; import cn.worstone.scripting.xml.XMLLanguageDriver; import cn.worstone.session.SqlSession; import cn.worstone.transaction.Transaction; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; public class Configuration { private DataSource dataSource; private final MapperRegistry mapperRegistry; private final Map<String, MappedStatement> mappedStatements; public Configuration() { this.mapperRegistry = new MapperRegistry(this); this.mappedStatements = new HashMap<>(); } public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public boolean hasMapper(Class<?> type) { return this.mapperRegistry.hasMapper(type); } public <T> void addMapper(Class<T> type) { this.mapperRegistry.addMapper(type); } public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return this.mapperRegistry.getMapper(type, sqlSession); } public Map<String, MappedStatement> getMappedStatements() { return mappedStatements; } // 创建执行器 public Executor newExecutor(Transaction transaction) { return new SimpleExecutor(this, transaction); } // 创建参数处理器 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return mappedStatement.getLanguageDriver().createParameterHandler(mappedStatement, parameterObject, boundSql); } // 创建结果集处理器 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, BoundSql boundSql) { return new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, boundSql); } // 创建 Statement 处理器 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return new PreparedStatementHandler(executor, mappedStatement, parameterObject, boundSql); } // 获取默认的语言驱动 public LanguageDriver getDefaultScriptingLanguageInstance() { return new XMLLanguageDriver(); } // 判断是否存在当前的 MappedStatement public boolean hasStatement(String statementName) { return this.mappedStatements.containsKey(statementName); } // 根据唯一标识获取 MappedStatement public MappedStatement getMappedStatement(String id) { return this.mappedStatements.get(id); } }
-
-
关于 Mapper 接口实现的方式
在课程里面这种方式的实现是直接通过在 getMapper 方法中使用匿名内部类实现的,而这里采用与 MyBatis 框架中一致的方式实现。
-
解析 XML 映射器的时候,直接创建 Mapper 代理对象,存储在Configuration 的 mapperRegister 中
private void bindMapperForNamespace() { if (this.namespace != null) { Class boundType = null; try { boundType = this.getClass().getClassLoader().loadClass(this.namespace); } catch (ClassNotFoundException e) { } if (boundType != null && !this.configuration.hasMapper(boundType)) { this.configuration.addMapper(boundType); } } }
-
SqlSession 中的 getMapper 方法直接从 Configuration 中获取对应的 Mapper 代理对象
@Override public <T> T getMapper(Class<T> mapperClass) { return this.configuration.getMapper(mapperClass, this); }
-
MapperProxy Mapper 接口的代理对象
package cn.worstone.binding; import cn.worstone.bean.Configuration; import cn.worstone.bean.MappedStatement; import cn.worstone.session.SqlSession; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; public class MapperProxy<T> implements InvocationHandler { private final SqlSession sqlSession; private final Class<T> mapperInterface; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; } @Override public Object invoke(Object proxy, Method method, Object[] args) { Configuration configuration = this.sqlSession.getConfiguration(); String methodName = method.getName(); String className = this.mapperInterface.getName(); String statement = className + "." + methodName; MappedStatement mappedStatement = configuration.getMappedStatement(statement); Object parameterObject = args == null ? null : args[0]; switch (mappedStatement.getSqlCommandType()) { case SELECT: Type returnType = method.getGenericReturnType(); if (returnType instanceof ParameterizedType) { return this.sqlSession.selectList(statement, parameterObject); } else { return this.sqlSession.selectOne(statement, parameterObject); } case INSERT: return this.sqlSession.insert(statement, parameterObject); case UPDATE: return this.sqlSession.update(statement, parameterObject); case DELETE: return this.sqlSession.delete(statement, parameterObject); default: throw new RuntimeException("Unknown execution method for: " + method.getName()); } } }
因为 Mapper 对象中封装了对应的 SqlSession 实例,所以我们可以从 SqlSession 中取出 Configuration ,然后再获取我们想要的一切东西。因为前面已经对 SQL 语句的参数类型进行了存储,所以这里直接对其进行判断,然后调用不同的方法即可。当然在 MyBatis 中实现不是这样简单,因为这里的查询结果集只有两种,但事实上,结果集也存在许多种类型, MyBatis 皆有相关的处理。有兴趣的同学可以翻阅一下 MapperMethod 类的源码,是上述部分功能的实现。
-
我的作业大致是这样的,但是其中还有许多问题,很多地方功能也不是特别完善,还是要不断地补充,在这里简单地聊一下后续的内容:
-
一级缓存的实现
-
异常处理部分的补全
@Test public void update() throws XPathExpressionException, IOException, ParserConfigurationException, SAXException, SQLException { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("SqlMapConfig.xml")); try(SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); Map<String, Object> parameterMap = new HashMap<>(4); parameterMap.put("id", "5f414d80-c413-42d4-8844-909d3e107ce8"); parameterMap.put("name", "xxx"); int row = userMapper.update(parameterMap); System.out.println(row); sqlSession.commit(); } }
细心的可能会发现这里抛出了大量的异常,其实在这部分,只需要保留 2 个异常即可,其他的异常都应该由框架处理,反馈给用户。
-
参数以及结果集处理
当前参数支持自定义实体类型、Map 类型以及 String 类型,需要补充其他类型参数的支持。对返回结果集目前也仅支持两种,这肯定是不行的,后续需要补充。
扩充下关于 MyBatis 多个数据源的实现方式