一、Mybatis底层工作原理
上篇文章中我们讲到Mybatis和数据库交互有两种方式,一是使用传统的Mybatis提供的API,第二是Mapper的代理方式。项目中一般都是使用第二种方式,那这两种方式的初始化过程有什么不同呢。
Mybatis在初始化的时候,会将Mybatis的配置信息全部加载到内存中,使用org.apache.ibatis.session.Configuration实例来维护。首先对Configuration对象介绍:
Configuration对象的结构和Mybatis核心配置文件sqlMapConfig.xml的对象几乎相同。回顾一下xml中的配置标签有哪些:properties(属性)、settings(设置)、typeAliases(类型别名)、typeHandlers(类型处理器)、pluins(拦截器)等。
Configuration也有对应的对象属性来封装它们。也就是说,初始化配置文件信息的本质就是创建Configuration对象并将解析的xml数据封装到Configuration内部属性中。
1、传统方式
sqlMapConfig.xml配置文件
<?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文件-->
<properties resource="jdbc.properties"></properties>
<plugins>
<plugin interceptor="com.lagou.plugin.MyPlugin">
<property name="name" value="tom"/>
</plugin>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
<!--environments:运行环境-->
<environments default="development">
<environment id="development">
<!--当前事务交由JDBC进行管理-->
<transactionManager type="JDBC"></transactionManager>
<!--当前使用mybatis提供的连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--引入映射配置文件-->
<mappers>
<!-- <mapper class="com.lagou.mapper.IUserMapper"></mapper>-->
<package name="com.lagou.mapper"/>
</mappers>
</configuration>
我们先编写一个测试类:
public void test1() throws IOException {
// 1. 读取配置文件,读成字节输入流,注意:现在还没解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. 解析配置文件,封装Configuration对象 创建DefaultSqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. 生产了DefaultSqlsession实例对象 设置了事务不自动提交 完成了executor对象的创建
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.(1)根据statementid来从Configuration中map集合中获取到了指定的MappedStatement对象
//(2)将查询任务委派了executor执行器
List<Object> objects = sqlSession.selectList("namespace.id");
// 5.释放资源
sqlSession.close();
}
(1)第一步,读取配置文件到字节输入流InputStream中(初始化过程)
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
(2)第二步,解析配置文件,封装Configuration对象 创建DefaultSqlSessionFactory对象(初始化过程)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
我们可以点进build方法看看,我们可以看到build方法中创建了一个xml解析器parser
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
// 创建 XMLConfigBuilder, XMLConfigBuilder是专门解析mybatis的配置文件的类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 执行 XML 解析
// 创建 DefaultSqlSessionFactory 对象
return build(parser.parse());
}
我们再点进parser.parse()中看看
public Configuration parse() {
// 若已解析,抛出 BuilderException 异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 标记已解析
parsed = true;
///parser是XPathParser解析器对象,读取节点内数据,<configuration>是MyBatis配置文件中的顶层标签
// 解析 XML configuration 节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
继续看parseConfiguration(parser.evalNode("/configuration"))方法,该方法会解析sqlMapConfig.xml中支持配置的所有标签,并封装到Configration中
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 <properties /> 标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载自定义的 VFS 实现类
loadCustomVfs(settings);
// 解析 <typeAliases /> 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins /> 标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory /> 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 赋值 <settings /> 到 Configuration 属性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers /> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers /> 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们重点看 mapperElement(root.evalNode("mappers")),该方法就是解析我们配置在sqlMapConfig中的映射文件并封装到Mybatis核心配置类Configration中
<!--引入映射配置文件-->
<mappers>
<!-- <mapper class="com.lagou.mapper.IUserMapper"></mapper>-->
<package name="com.lagou.mapper"/>
</mappers>
我们来重点看看Configration这个类中的
介绍一下MappedStatement:MappedStatement与Mapper配置文件中的一个select/update/insert/delete节点相对应。mapper中配置的 标签都被封装到了此对象中。比如id,resultType、paramterType还有最重要的sql语句。
<select id="findByCondition" resultType="com.lagou.pojo.User" paramterType="com.lagou.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
(3)第三步,生产了DefaultSqlsession实例对象 设置了事务不自动提交 完成了executor对象的创建(sql执行过程)
SqlSession sqlSession = sqlSessionFactory.openSession();
先简单介绍下SqlSession:
SqlSession是一个接口,它有两个实现类:DefaultSqlSession(默认)和SqlSessionManager(弃用),SqlSessionManager是Mybatis中用于和数据库交互的顶层类,通常将它与ThreadLocal绑定,一次会话使用一个SqlSession,并且在使用完后需要close。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
}
SqlSession中最重要的两个参数,configuration与初始化时相同,Executor为执行器
Executor也是一个接口,他有三个常用的实现类:BatchExecutor(重用语句并执行批量更新)、ReuseExecutor(重用预处理语句preparedstatements)、SimpleExecutor(普通的执行器,默认)。
我们点进openSession()方法中看看:获取了Executor类型、并设置了事务不自动提交
//6. 进入openSession方法
@Override
public SqlSession openSession() {
//getDefaultExecutorType()传递的是SimpleExecutor
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
继续看看openSessionFromDataSource()方法:我们可以看到方法中创建了执行器Executor和DefaultSqlSession对象
(4)第四步,根据statementid来从Configuration中map集合中获取到了指定的MappedStatement对象,将查询任务委派了executor执行器(Executor代码剖析)
List<Object> objects = sqlSession.selectList("namespace.id");
我们点进selectList方法:
点进query()方法中我们可以看到,先创建了缓存key
继续看query()方法:缓存中没有数据则从数据库中查,调用queryFromDatabase()方法
再看queryFromDatabase()方法:缓存我们已经在之前文章讲过了,这里重点看sql执行操作
进doQuery()看看:这里创建了StatementHandlder对象,并将查询操作交给了StatementHandlder,这也验证了我们上篇文章中讲的Mybatis的层次结构(Executor又交给了StatementHandlder去执行)
我们先看prepareStatement()方法:parameterHandler完成对参数设值
我们再看handler.query()方法:由resultSetHandler处理返回结果
2,Mapper的代理方式
Mapper的代理方式和传统方式前三部相同,我们先编写个测试类
/**
* mapper代理方式
*/
public void test2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
// 使用JDK动态代理对mapper接口产生代理对象
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
//代理对象调用接口中的任意方法,执行的都是动态代理中的invoke方法
List<Object> allUser = mapper.findAllUser();
}
(4)第4步,使用JDK动态代理对mapper接口产生代理对象
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
在看getMapper()方法之前,我们先回去看看Mybatis初始化解析文件方法中解析mappers标签这块
我们重点看这一段:当我们配置了package标签,即该包下的所有Mapper文件都会被扫描时,会调用configuration.addMappers()方法
点进addMappers()方法可以看到,初始化时会扫描包下所有Mapper接口(即Dao接口),并添加到mapperRegistry中
我们查看MapperRegistry这个类可以看到里面有个HashMap,该key值就是Mapper接口类型,MapperProxyFactory就是接口代理工厂,一个Mapper接口对应MapperProxyFactory代理工厂
我们回到sqlSession.getMapper方法中:
在继续往下看:我们可以看到根据传的Class类型(key)直接获取到了对应的接口代理工厂MapperProxyFactory
我们再看mapperProxyFactory.newInstance()方法:创建了MapperProxy类,该类实现了JDK动态代理的InvocationHandler接口
//MapperProxyFactory类中的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 创建了JDK动态代理的invocationHandler接口的实现类mapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 调用了重载方法
return newInstance(mapperProxy);
}
我们继续看 newInstance(mapperProxy)方法:这里我们就证实了IUserMapper mapper = sqlSession.getMapper(IUserMapper.class)返回的是JDK动态代理对象
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
(5)第五步,代理对象调用接口中的任意方法,执行的都是动态代理中的invoke方法
List<Object> allUser = mapper.findAllUser();
我们找到MapperProxy中的invoke方法:这里是由MapperMethod 对象调用了执行的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是 Object 定义的方法,直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 获得 MapperMethod 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 重点在这:MapperMethod最终调用了执行的方法
return mapperMethod.execute(sqlSession, args);
}
我们点进execute()方法中,发现该方法增删改查都有,并且实质在最终和传统方式一样还是由sqlSession执行了查询:sqlSession.selectList()。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断mapper中的方法类型,最终调用的还是SqlSession中的方法
switch (command.getType()) {
case INSERT: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 执行 INSERT 操作
// 转换 rowCount
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 无返回,并且有 ResultHandler 方法参数,则将查询的结果,提交给 ResultHandler 进行处理
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 执行查询,返回列表
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 执行查询,返回 Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
// 执行查询,返回 Cursor
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
// 执行查询,返回单个对象
} else {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 查询单条
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;