Mybatis源码详解
- 一、JDBC与Mybatis对比
- JDBC调用
- Mybatis调用
- 两者对比
- 二、Mybatis资源加载
- 数据源获取
- SqlSessionFactoryBuilder.build
- XMLConfigBuilder.parse
- XMLConfigBuilder.environmentsElement
- SQL语句获取
- 1.入口
- 2.两种方式
- 3.XML方式获取SQL
- 3.1 XMLMapperBuilder.parse()
- 3.2 XMLMapperBuilder.configurationElement
- 3.4 XMLMapperBuilder.buildStatementFromContext
- 3.5 XMLStatementBuilder.parseStatementNode
- 3.6 MapperBuilderAssistant.addMappedStatement
- 4.注解的方式获取SQL
- 4.1 MapperRegistry.addMapper
- 4.2 MapperAnnotationBuilder.parse
- 4.3 MapperAnnotationBuilder.parseStatement
- 5.最终的SQL处理方法
- XMLLanguageDriver.createSqlSource
- 动/静SQL判断
- 静态SQL处理
- 6.如果两种配置方式我都配了会怎样?
- 先走Xml解析,后走注解的方式
- 先走注解的方式,后走Xml的解析
- Mapper代理对象获取
- 命名空间加载入口
- 包/类名加载入口
- 总结
- 三、Mybatis语句执行
- 1.代理对象的生成
- 2.代理对象的执行方法
- 3.方法的执行
- 4.查询一条为例
- 5.执行器执行
- 5.1 获取StatementHandler
- 5.2 获取连接并获取SQL
- 5.2.1 预处理Statement
- 5.2.2 SQL传参处理
- 5.3 执行并处理返回结果
- 6.总结
- 四、总结
- 核心部件
- 遗漏的知识点
本文用的是3.5.10版本
源码地址:https://github.com/mybatis/mybatis-3/releases
文档地址:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html
环境的搭建本文不做阐述了,文档里面有,本文适用已经对Mybatis使用有一定了解的人阅读!
一、JDBC与Mybatis对比
我们先看看Mybatis调用Mysql和JDBC调用Mysql有什么区别?这样就知道Mybatis帮我们做了哪些事了
JDBC调用
- 加载驱动后,先获取连接(数据源)
- 获取操作对象(预编译)
- 传参处理
- 执行
- 返回值处理
Connection connection =null;
try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//1.获取连接 数据源
connection = DriverManager.getConnection(url, user, password);
//2.获取操作对象
PreparedStatement preparedStatement = connection.prepareStatement("select * from test_user where id=?");
//3.传参处理
preparedStatement.setInt(1,1);
//4.执行
ResultSet resultSet = preparedStatement.executeQuery();
//5.返回值处理
while (resultSet.next()){
TestVO testVO = new TestVO(
resultSet.getInt("id"),
resultSet.getString("name"),
resultSet.getString("team"),
resultSet.getInt("grade")
);
System.out.println(JSONObject.toJSON(testVO));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(connection!=null){
connection.close();
}
}
Mybatis调用
加载驱动就不说了,总的分几步:
- 加载配置文件把资源放入工厂
- 从工厂获取一个会话(会话就可以理解成是个连接)
- 从会话里面获取动态代理的Mapper对象执行对应方法就可以了
// 获取数据源 获取SQL语句 获取Mapper 传参处理 执行 返回值处理
try {
String resource = "resources/mybatis-config.xml";
// 通过classLoader获取到配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 获取会话工厂
SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
// 获取一个会话
SqlSession sqlSession = sqlSessionFactory.openSession();
// 动态代理方式
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
List<TestVO> dataList = mapper.findDataList(1,"张三");
System.out.println("通过动态代理返回结果" + JSONObject.toJSON(dataList));
sqlSession.close();
} catch (Exception e) {
e.printStackTrace();
}
两者对比
首先要达成一个共识,Mybatis最终调用Mysql的方式和JDBC是不是一样的?
是一样的,所以Mybatis只是对JDBC做了一层封装,帮我们简化了很多操作,比如:
- SQL传参的自动适配处理(JDBC传参还要传参与SQL参数索引对应)
- 返回值处理自动映射处理(JDBC返回值还要自己与实体类映射吧)
最主要就这两个吧,SQL还是需要我们自己写,连接、预编译、执行器这些JDBC都有吧!
所以为什么Mybatis被称为半ORM框架这些懂了吧?就是SQL还需要自己写,传参和返回值处理它帮你搞定了
所以我们总结一下Mybatis做了什么?怎么帮我们做的封装,接下来分步骤一步一步看:
- 一、资源的获取
- 数据源获取
- SQL语句获取
- Mapper代理对象获取
- 二、执行
- 传参处理
- 语句执行
- 返回值处理
二、Mybatis资源加载
资源的获取就是XML文件的解析,我们再回顾一下Mybatis的代码,是不是加载了一份资源文件
数据源获取
数据源就是XML配置文件里面environments标签部分,我们需要解析并加载,如下图所示:
SqlSessionFactoryBuilder.build
XMLConfigBuilder.parse
找到我们需要解析的environments标签,就是数据源的配置解析
XMLConfigBuilder.environmentsElement
还通过了遍历获取,说明数据源可以有多个,然后数据源被放到了Configuration里面,到这数据源就解析完成了
(这个Configuration记住哈,资源的解析完了后都是放到这里面的,很重要)
SQL语句获取
1.入口
XMLConfigBuilder.parse
和上面一样同样在解析XML配置文件里面
2.两种方式
我们看看配置文件里面mappers标签是怎么配置的,官方给出了四种配置方式:
- Mapper.xml文件的相对路径引用(解析XML获取SQL)
- Mapper.xml文件的绝对路径引用(解析XML获取SQL)
- Mapper.java类文件的完全限定类名(解析注解获取SQL)
- Mapper.java类文件所在的包路径(解析注解获取SQL)
既然配置有4种配置方法,那对应的解析的时候也有4种解析方法:
XMLConfigBuilder.mapperElement
所以我们要知道SQL语句有两个地方可以获取(应该都知道霍,SQL本身就可以写两个地方)
- 加载XML资源文件获取
- 加载Mapper接口文件通过注解的形式获取
所以接下来我们要分两种方式来解析!
3.XML方式获取SQL
从入口处看,XML方式都会调用
3.1 XMLMapperBuilder.parse()
3.2 XMLMapperBuilder.configurationElement
有很多标签就不一一看了,直接看增删改查的标签
3.4 XMLMapperBuilder.buildStatementFromContext
遍历XML节点一个一个获取
3.5 XMLStatementBuilder.parseStatementNode
这个方法有点长我们分三个截图,分别代表三个关键的点,一个一个来
第一段: 这个是干嘛的,我们后面说(判断重复加载)
第二段:这就是获取SQL资源的地方,我们一样后面说,因为通过注解的方式解析的时候也会调用这个方法,后面一起说,先记着是LanguageDriver.createSqlSource方法
第三段:这个可以是最后的一步了,我们的数据源被加载放到了Configuration里面,所以SQL相关的也不例外要放入Configuration中
3.6 MapperBuilderAssistant.addMappedStatement
最终被封装成了MappedStatement放入了Configuration中
4.注解的方式获取SQL
我们一样从入口开始看
4.1 MapperRegistry.addMapper
两个入口一直往下都会到这个方法,中间的就不看了,没啥好说的
4.2 MapperAnnotationBuilder.parse
中间要做什么处理,搞什么鬼的一律不理,我们直奔主题
4.3 MapperAnnotationBuilder.parseStatement
这个方法有点长我们同样分几段
第一段:第一步获取SQL资源,这个buildSource一样会到我们上面说的LanguageDriver.createSqlSource方法,后面一起说
第二段:是不是有点熟悉又是MapperBuilderAssistant.addMappedStatement方法,和上面一样的最后被封装成MappedStatement放入了Configuration中(和上面一样就不说了)
5.最终的SQL处理方法
我们解析XML的时候或者解析注解的时候难道拿不到SQL吗?还需要单独去解析获取SQL?
这里的SQL资源获取并不代表是获取XML或者注解里面的SQL,恰恰是对里面的SQL处理一次,我们最终需要的SQL是像JDBC里面那样参数是 "?"的SQL格式,所以这里的处理是将XML或者注解里面SQL中的占位符处理掉,并建立占位符下标索引和传参参数的映射
如下图所示:
处理后SQL已经变成了最后执行所需的样子,同时产生了占位符的下标索引和传参的映射关系的ParameterMapping对象
所以需要处理两种情况:
- 带${}占位符:处理方式是直接用传参直接替换
- 带#{}占位符:处理方式是需要将占位符变成 “?”,然后再用传参顺序替换 “?”
上面我们说到createSqlSource方法,实际是在XMLLanguageDriver中,存在两个重载方法
XMLLanguageDriver.createSqlSource
不管是注解形式的SQL还是XML形式的SQL最终都会到这,两者解析完最终都会判断SQL是静态SQL还是动态SQL,有不同的处理方法
- 静态SQL:SQL中不存在${}占位符,采用RawSqlSource类处理
- 动态SQL:SQL中存在${}占位符或者像XML中if动态判断标签,采用DynamicSqlSource类处理
动/静SQL判断
TextSqlNode.isDynamic()
该方法中会调用GenericTokenParser类进行对SQL中"${}"占位符的处理,而处理方法正是DynamicCheckerTokenParser.handleToken方法,该方法中将标志位设置为了True,而这种动态的SQL会在最终执行的时候才去处理(用传参替换)
动态SQL一共有两种,一是${}占位符,二是XML中存在IF标签等判断语句(这种大家可以从XML入口去看)
静态SQL处理
RawSqlSource
静态的SQL会在该类中的构造方法中直接处理了
SqlSourceBuilder.parse()
相比于动态SQL,处理的Handler换了,看替换的本文变成了"?",同时还加上了下标的映射关系
GenericTokenParser这个是占位符的处理类,就等于是一个工具类,这里就不贴了,有兴趣的可以去看看,实际开发说不定也能用得上哦
6.如果两种配置方式我都配了会怎样?
先走Xml解析,后走注解的方式
结果会报错,因为注解的方式后解析,而在注解的方式解析过程中并没有判断MappeStatement是否已经存在,此时会继续往Configuration中添加MappeStatement,而Configuration里面用的Map是自定义的StrictMap,put相同的key会报错
先走注解的方式,后走Xml的解析
无事发生,因为在xml解析过程中有判断MappeStatement是否已经存在
(这就是我上面说截图第一段后面说的,至此坑已经填完)
Mapper代理对象获取
应该都知道Mapper的执行原理是动态代理,所以Mapper也需要加载变成代理对象,结合SQL的获取方式,Mapper加载也有两种
- XML方式加载:通过XML里面配置的命名空间获取Mapper对象
- 注解方式加载:配置的是包名或者全类名所以直接直接获取Mapper对象
命名空间加载入口
就在XML解析的下面
XMLMapperBuilder.bindMapperForNamespace()
会调用addMapper()这个方法
包/类名加载入口
就在加载的时候,也会调用addMapper()方法,我们直接看这个方法
MapperRegistry.addMapper()
先判断是否存在,不存在就直接加入到了knownMappers这个Map对象中,加入的是一个代理工厂
MapperProxyFactory,然后就没了,等着执行时调用
总结
- 在配置文件加载过程中,加载了数据源、SQL资源、Mapper代理工厂
- SQL分为动态SQL和静态SQL,静态SQL是在加载过程中就处理好了,动态SQL需要在执行时获取参数才能处理(带${}占位符或者IF等判断标签)
- Mapper会生成其代理工厂保存在一个Map对象中
- MappedStatement保存着所有SQL相关资源(SQL语句、参数映射关系等)
- 除了Mapper代理对象,其他资源都会放入Configuration中
三、Mybatis语句执行
应该都知道,前面也说过获取的Mapper是一个代理对象,最后的执行是代理对象的执行方法,所以我们直接点进去看看代理对象的执行方法是什么,先看看代理对象如何生成的
1.代理对象的生成
MapperRegistry.getMapper
我们随着getMapper方法一路进来会找到该方法,一眼过去非常的眼熟是不是,先从Mapper集合取出代理工厂,然后用代理工厂去生成代理对象,这里的SqlSession是什么?就是DefaultSqlSession,里面就包含之前装载资源的Configuration
采用的就是JDK的动态代理,所以这个MapperProxy很明显里面就有最终代理对象的处理方法了
2.代理对象的执行方法
MapperProxy.invoke
很容易就找到了方法执行处,忽略掉一系列判断,我们直接找到最终的处理方法
又采用了一个静态代理的方式调用了执行
3.方法的执行
MapperMethod.execute
到了这里是不是就顺眼多了,熟悉的增删改查,我们以查为例,查还分无返回值、查多条、查Map、查一条这些情况,我们就简单一点以差一条为例子走
4.查询一条为例
DefaultSqlSession.selectOne
进来这里你看看这个类下的方法,虽然是以查询一条为例,但是增删改查操作最终都会到这,而且你看看查询一条的时候实际也是查询的是list,最后只是返回了一条结果而已,下面那个报错异常相信大家也很熟悉吧
DefaultSqlSession.selectList
顺着上面下来,这里会先获取一个MappedStatement,这个是什么东西?还记得上面说的SQL资源获取吗?这里面就是SQL相关的所有资源,比如SQL语句、返回值、传参映射等等,所以这里的获取指的就是去获取现在要执行的方法对应的SQL资源(MappedStatement),通过这个方法的全路径类名+方法名称获取,然后再调用执行器执行查询!
5.执行器执行
执行这块涉及到两个知识点,一是Mybatis的二级缓存,二是Mybatis的三大执行器
这里我们先跳过这两个点,直接来看默认的执行器(SimpleExecutor)执行过程
SimpleExecutor.doQuery
这里分三步
- 获取具体的操作类,这里的操作类就类似于JDBC中的PreparedStatement
- 获取最后要执行的SQL(处理传参)、获取连接并设置相关参数(超时时间等)
- 执行并处理返回结果
5.1 获取StatementHandler
Configuration.newStatementHandler
这里主要就是选择采用哪个StatementHandler
RoutingStatementHandler:
StatementHandler的选择是根据参数来的,这个参数在XML里面是可以配置的,默认是PreparedStatementHandler
5.2 获取连接并获取SQL
SimpleExecutor.prepareStatement
- 先获取连接
- 预处理一下Statement(设置超时时间、获取SQL)
- SQL传参处理
5.2.1 预处理Statement
BaseStatementHandler.prepare
获取连接就不看了,没啥看的,直接看看预处理,主要关注这个初始化
PreparedStatementHandler.instantiateStatement
看这是不是跟JDBC里面第二步一模一样?现在就只差传参处理、执行、返回值处理了是不是?
5.2.2 SQL传参处理
PreparedStatementHandler.parameterize
DefaultParameterHandler.setParameters
这个就是处理传参的,根据之前处理后的映射关系,找到对应类型的处理类处理参数,然后与JDBC类型映射,这整个过程都是自动完成的,所以这里的参数处理还需要所有类型的对应处理方式是不是,一旦处理方式没匹配上就会有问题对吧
TypeHandler
所以Mybatis里面列举了很多类型的处理方式,而且看方法,不仅处理参数,还处理返回值
5.3 执行并处理返回结果
PreparedStatementHandler.query
DefaultResultSetHandler.handleResultSets
获取了JAVA中与JDBC参数类型的映射关系、获取XML中的配置、然后遍历处理结果集
ResultSetWrapper
JAVA中与JDBC参数类型的映射关系如下
6.总结
以查询一条、SimpleExecutor执行器为例:
- 首先会获取Mapper代理对象
- 然后判断要执行的是增、删、改、查其中的哪种
- 然后选择执行器执行(默认SimpleExecutor)
- 然后选择操作处理类StatementHandler
- 获取连接、获取SQL
- 传参处理,然后执行,然后返回值处理
这里网上找了图,结构上更明了:
四、总结
核心部件
从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:
- Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
- SqlSessionFactory:SqlSession工厂。
- SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
- Executor: MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
- StatementHandler: MyBatis直接在数据库执行SQL脚本的对象。
- ParameterHandler: 负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
- ResultSetHandler: 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
- TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
- MappedStatement: MappedStatement维护了SQL相关资源
- SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
- BoundSql:表示动态生成的SQL语句以及相应的参数信息SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
遗漏的知识点
这三个可能有些人平时用的少,但都算是Mybatis提供的一些功能,后续单独拿出来说
- Mybatis二级缓存
- 三大执行器
- 扩展功能之Mybatis拦截器