这个系列的文章的开篇《当我们说起看源码时,我们是在看什么》在去年十月份就开始了,今天开始填这个系列的坑。MyBatis是我接触的第一个ORM框架,也是我目前最为熟悉的ORM框架,对它一直停留在用的阶段,今天尝试来看MyBatis的内部构造。如果还不会MyBatis的,可以先去看下《假装是小白之重学MyBatis(一)》。
那该如何看源码呢?
我是把MyBatis的源码下载下来, 茫无目的的看?这会不会迷失在源码中呢,我记得我刚到我当前这家公司的时候,看代码就是一个一个方法的看,然后感觉很头疼,也没看懂最后再做什么。后面反思了一下,其实应该关注宏观的流程,就是这个代码实现了什么功能,这些代码都是为了实现这个功能,不必每一个方法都一行一行的看,以方法为单位去看,这个方法从整体上来看做了什么样的事情,先不必过多的去关注内部的实现细节。这样去看对代码大概心里就有数了。同样的在MyBatis这里,这也是我第一个特别仔细研究的代码,所以MyBatis系列的第一篇,我们先从宏观上看其实现,在后面的过程中慢慢补全其细节。本篇的主线是我们在xml中写的增删改查语句究竟是怎么被执行的。
参阅了很多MyBatis源码的资料,MyBatis的整体架构可以分为三层:
- 接口层: SqlSession 是我们平时与MyBatis完成交互的核心接口(包括后续整合SpringFramework用到的SqlSessionTemplte)
- 核心层: SqlSession执行的方法,底层需要经过配置文件的解析、SQL解析,以及执行SQL时的参数映射、SQL执行、结果集映射,另外还有穿插其中的扩展插件。
- 支持层: 核心层的功能实现,是基于底层的各个模块,共同协调完成的。
搭建MyBatis的环境
搭建MyBatis的环境在《假装是小白之重学MyBatis(一)》已经讲过了,这里只简单在讲一下:
- 引入Maven依赖
org.mybatis
mybatis
3.5.6
mysql
mysql-connector-java
5.1.47
com.alibaba
druid
1.2.5
org.slf4j
slf4j-api
1.7.30
org.slf4j
slf4j-log4j12
1.7.30
- 然后来一张表
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的配置文件
- 来个Student类
public class Student {
private Long id;
private String name;
private String number;
private String money;
// 省略get set 函数
}
- 来个Mapper.xml
- 来个接口
public interface StudentMapper {
List 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 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 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 Object executeForMany(SqlSession sqlSession, Object[] args) {
List 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 List selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
//
@Override
public List 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 List 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 List 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...