共计 14081 个字符,预计需要花费 36 分钟才能阅读完成。
这个系列的文章的开篇《当咱们说起看源码时,咱们是在看什么》在去年十月份就开始了,明天开始填这个系列的坑。MyBatis 是我接触的第一个 ORM 框架,也是我目前最为相熟的 ORM 框架,对它始终停留在用的阶段,明天尝试来看 MyBatis 的外部结构。如果还不会 MyBatis 的,能够先去看下《伪装是小白之重学 MyBatis(一)》。
那该如何看源码呢?
我是把 MyBatis 的源码下载下来, 茫无目标的看?这会不会迷失在源码中呢,我记得我刚到我以后这家公司的时候,看代码就是一个一个办法的看,而后感觉很头疼,也没看懂最初再做什么。前面反思了一下,其实应该关注宏观的流程,就是这个代码实现了什么性能,这些代码都是为了实现这个性能,不用每一个办法都一行一行的看,以办法为单位去看,这个办法从整体上来看做了什么样的事件,先不用过多的去关注外部的实现细节。这样去看对代码大略心里就无数了。同样的在 MyBatis 这里,这也是我第一个特地认真钻研的代码,所以 MyBatis 系列的第一篇,咱们先从宏观上看其实现,在前面的过程中缓缓补全其细节。本篇的主线是咱们在 xml 中写的增删改查语句到底是怎么被执行的。
参阅了很多 MyBatis 源码的材料,MyBatis 的整体架构能够分为三层:
- 接口层:SqlSession 是咱们平时与 MyBatis 实现交互的外围接口 (包含后续整合 SpringFramework 用到的 SqlSessionTemplte)
- 核心层: SqlSession 执行的办法,底层须要通过配置文件的解析、SQL 解析,以及执行 SQL 时的参数映射、SQL 执行、后果集映射,另外还有交叉其中的扩大插件。
- 反对层: 核心层的性能实现,是基于底层的各个模块,独特协调实现的。
搭建 MyBatis 的环境
搭建 MyBatis 的环境在《伪装是小白之重学 MyBatis(一)》曾经讲过了,这里只简略在讲一下:
- 引入 Maven 依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
- 而后来一张表
CREATE TABLE `student` (`id` int(11) NOT NULL COMMENT '惟一标识',
`name` varchar(255) ,
`number` varchar(255) ,
`money` int(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
- 来个 MyBatis 的配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载配置文件 -->
<properties resource="jdbc.properties"/>
<!-- 指定默认环境, 个别状况下, 咱们有三套环境,dev 开发 ,uat 测试 ,prod 生产 -->
<environments default="development">
<environment id="development">
<!-- 设置事务管理器的治理形式 -->
<transactionManager type="JDBC"/>
<!-- 设置数据源连贯的关联形式为数据池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/studydatabase?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 设置扫描的 xml,org/example/mybatis 是包的全类名,StudentMapper.xml 会讲 -->
<mappers>
<!-- 设置扫描的 xml,org/example/mybatis 是包的全类名,这个 BlogMapper.xml 会讲
<package name = "org.example.mybatis"/> <!-- 包下批量引入 单个注册 -->
<mapper resource="org/example/mybatis/StudentMapper.xml"/>
</mappers>
</mappers>
</configuration>
- 来个 Student 类
public class Student {
private Long id;
private String name;
private String number;
private String money;
// 省略 get set 函数
}
- 来个 Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "org.example.mybatis.StudentMapper">
<select id = "selectStudent" resultType = "org.example.mybatis.Student">
SELECT * FROM STUDENT
</select>
</mapper>
- 来个接口
public interface StudentMapper {List<Student> selectStudent();
}
- 日志配置文件
log4j.rootCategory=debug, CONSOLE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30
- 开始查问之旅
public class MyBatisDemo {public static void main(String[] args) throws Exception {Reader reader = Resources.getResourceAsReader("conf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
List<Student> studentList = studentMapper.selectStudent();
studentList.forEach(System.out::println);
}
}
执行之后就能够在控制台看到如下输入了:
让咱们从 SQL 的执行之旅开始谈起
执行过程浅析
下面的执行过程大抵能够分成三步:
- 解析配置文件,构建 SqlSessionFactory
- 通过 SqlSessionFactory 拿到 SqlSession,进而取得代理类
- 执行代理类的办法
解析配置文件
解析配置文件通过 SqlSessionFactoryBuilder 的 build 办法来执行, build 办法有几个重载:
Reader 指向了 conf 文件, environment 是环境,properties 用于 conf 向其余 properties 取值。咱们的配置文件是一个 xml,所以 XmlConfigBuilder 最终是对配置文件的封装。这里咱们不关注 XmlBuilder 是如何构建的,咱们接着往下看,构建 Xml 对象之后,调用 parse 办法,将其转换为 MyBatis 的 Configuration 对象:
// parseConfiguration 这个办法用于取 xml 标签的值并将其设置到 Configuration 上
public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 取标签的过程,XML->Configuration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers")); // 获取 mapper 办法,
} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause:" + e, e);
}
}
- Configuration 概览
- mapperElement
留神咱们本篇的主题是重点看咱们写在 xml 标签中的 sql 是如何被执行的,所以咱们这里重点看 parseConfiguration 的 mapperElement 的办法。从名字上咱们大抵推断,这个办法是加载 mapper.xml 文件的。咱们点进去看一下:
// parent 是 mappers 标签
private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) { // 遍历 mappers 上面的结点
if ("package".equals(child.getName())) { // 如果是 package 标签则走批量引入
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {String resource = child.getStringAttribute("resource"); // 咱们本次看单个引入的形式
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource); // 加载指定文件夹下的 XML
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse(); // 将 mapper 中的标签值映射成 MyBatis 的对象} else if (resource == null && url != null && mapperClass == null) {ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse(); // 咱们大抵看下 parse 办法的实现} else if (resource == null && url == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
parent 参数是 mappers 标签,咱们能够通过调试验证这一点:
public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();}
在介绍的时候西安判断该 xml 是否曾经加载过了, 而后解析 mapper 标签下的增删改查等标签,咱们能够在 configurationElement 看到这一点。
private void configurationElement(XNode context) {
try {String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 该办法解析标签
} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is'" + resource + "'. Cause:" + e, e);
}
}
private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) { // dataBaseId 用于指明该标签在哪个数据库下执行
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
parseStatementNode 办法比拟长,最终还是在解析 Mapper.xml 的 select、insert、update、delete 的属性, 将解析的属性传递 builderAssistant.addMappedStatement() 办法中去,该办法参数略多,这来咱们上截图:
到此咱们根本完结看构建 configuration 的过程,咱们能够认为在这一步,Mybatis 的配置文件和 Mapper.xml 曾经根本解析结束。
获取 SqlSession 对象
SqlSession 是一个接口,有两个次要实现类:
咱们在第一步 build 进去的事实上是 DefaultSqlSessionFactory:
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}
这里事实上 openSession 也是由 DefaultSqlSessionFactory 来执行的,咱们看下在 openSession 这个过程中大抵做了什么:
@Override
public SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
留神这个 getDefaultExecutorType, 这个事实是 MyBatis 分层中核心层的 SQL 执行器,咱们接着往下看 openSessionFromDataSource:
// level 隔离级别, autoCommit 是否主动提交
// ExecutorType 是一个枚举值: SIMPLE、REUSE、BATCH
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 返回一个执行器, 咱们看下 newExecutor 这个办法
final Executor executor = configuration.newExecutor(tx, execType);
// 最初结构进去 SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause:" + e, e);
} finally {ErrorContext.instance().reset();}
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);
} else {executor = new SimpleExecutor(this, transaction);
}
// 下面是依据 executorType 生成对应的执行器
// 如果开启缓存,则将其执行器包装为另一种模式的执行器
if (cacheEnabled) {executor = new CachingExecutor(executor);
}
// interceptorChain 是一个拦截器链
// 将该执行器退出到拦截器链中加强, 这事实上是 MyBatis 的插件开发。// 也是装璜器模式的利用, 前面会讲。executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
执行增删改查
接着咱们来看咱们的接口中的办法是如何执行的,
StudentMapper 执行 selectStudent 办法事实上进入的应该是对应代理的对象, 咱们进入下一步, 事实上是进入了 invoke 办法,这个 invoke 办法事实上重写的 InvocationHandler 的办法,InvocationHandler 是 JDK 提供的动静代理接口,调用被代理的的办法,事实上是会走到这个 invoke 办法中,实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);
} else {
// 该办法会缓存该办法, 如果该在缓存外面有,则无需再次产生, 外面的 methodCache 是 ConcurrentHashMap
// 最终会返回 MapperMethod 对象调用 invoke 办法。// 我这里最终的 MethodInvoker 是 PlainMethodInvoker
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
}
}
最终的 invoke 办法如下图所示:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 这个 execute 太长, 外面依据标签类型, 来做下一步的操纵, 这里咱们放截图
return mapperMethod.execute(sqlSession, args);
}
咱们接着来跟 executeForMany 这个办法的执行:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
// 默认的分页
if (method.hasRowBounds()) {RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
// 会走 DefaultSqlSession 的 selectList 上面
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
// 转换后果
if (!method.getReturnType().isAssignableFrom(result.getClass())) {if (method.getReturnType().isArray()) {return convertToArray(result);
} else {return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
//
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 这个 statement 是办法援用:org.example.mybatis.StudentMapper.selectStudent
// 通过这个 key 就能够从 configuration 获取构建的 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// query 外面会判断后果是否在缓存里, 咱们没有引入缓存
// 最终会走的 query 中的 queryFromDatabase 办法。// queryFromDatabase 外面会调用 doQuery 办法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause:" + e, e);
} finally {ErrorContext.instance().reset();}
}
咱们这里重点来看 doQuery 办法:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 这里咱们其实曾经能够看到 MyBatis 曾经筹备在调用 JDBC 了
// Statement 就位于 JDBC 中
Statement stmt = null;
try {Configuration configuration = ms.getConfiguration();
// 依据参数解决标签中的 SQL
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 产生执行 SQL 的 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 接着调 query 办法. 最终会走到 PreparedStatementHandler 的 query 办法上
return handler.query(stmt, resultHandler);
} finally {closeStatement(stmt);
}
}
// 最终执行 SQL
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
PreparedStatement 是 JDBC,到当初就曾经开始调用 JDBC 执行 SQL 了。resultSetHandler 是对 JDBC 后果进行解决的处理器。
这里咱们把下面遇到的 Handler 大抵梳理一下:
- StatementHandler: 语句处理器
- ResultSetHandler:后果处理器,有后果处理器就有参数处理器
- ParameterHandler: 参数处理器,
总结一下
在 MyBatis 中咱们写在 xml 文件中的 SQL 语句到底是怎么被执行这个问题到当初曾经有了答案:
- xml 中的查问语句、属性会被事后的加载进入 Configuration 中,Configuration 中有 MappedStatements,这是一个 Map,key 是标签的 id。
- 咱们在执行对应的 Mapper 的时候,首先要执行获取 Session,在这个过程中会通过 MyBatis 的拦截器,咱们能够抉择在这个过程对 MyBatis 进行加强
- 调用接口对应的办法时, 事实上调用的时代理类的办法,代理类会先进行参数解决,依据办法签名获取 MappedStatement,再转换,交给 JDBC 来解决。
到当初咱们曾经对 MyBatis 曾经有的执行流程曾经有一个大抵的理解了,可能一些办法没有看太细,因为讲那些细节也对宏观执行流程没有太大的帮忙。
参考资料
- MyBatis 视频教程(高级篇)视频 颜群 https://www.bilibili.com/vide…
- 玩转 MyBatis:深度解析与定制 https://juejin.cn/book/694491…