MyBatis源码解析

大致流程

MyBatis的基本使用流程为:

  • 创建SqlSessionFactory
  • 创建SqlSession
  • 通过SqlSession创建Mapper代理
  • 使用Mapper代理执行Mapper接口方法。

下面将针对这四步进行源码解析。源码版本为3.3.0。

创建SqlSessionFactory

SqlSessionFactory顾名思义,就是用来创建SqlSession的。创建语句也很简单

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

其中build方法就是读取xml配置并生成Configuration对象,并将其赋给SqlSessionFactory对象。这里仅展示两个核心方法:

{% codeblock lang:Java %} public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 开始解析,从configuration根结点开始 parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { // 解析是有先后顺序的,所以写xml的时候需要注意子结点的顺序 propertiesElement(root.evalNode("properties")); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); ... } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } {% endcodeblock %}

创建SqlSession

SqlSession是mybatis的核心,既可以执行SQL并返回结果,也可以获取Mapper对象。创建语句为:

SqlSession sqlSession = sqlSessionFactory.openSession();

再往里看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
// 这里的Transaction是对DB一个connection的包装
// 处理connection的生命周期,即创建,准备,提交/回滚和关闭。
Transaction tx = null;
try {
// 找到配置的默认environment,具体可以看xml中的environments结点的default属性
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// Executor是执行的实体
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}

代码逻辑也比较简单,需要注意的是直到这里,还是没有真正建立JDBC连接。

获得Mapper代理

Mapper代理的获得也很简洁:

XXXMapper mapper = sqlSession.getMapper(XXXMapper.class);

那么Mapper代理是如何获得的呢,总结来说就是通过JDK动态代理生成的,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
/*
** knownMappers的类型是Map<Class<?>, MapperProxyFactory<?>>
** 内部存储的是Mapper类型和相应的Mapper代理工厂
** 这些Mapper类型都是在xml文件中的mappers结点定义的
*/
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

public T newInstance(SqlSession sqlSession) {
// mapperProxy的invoke方法很关键
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
// 返回最后的代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

关于这部分,其实一开始有个小疑问,既然MyBatis把缓存发挥到了极致,那为什么不缓存MapperProxy和Mapper代理对象呢?我个人觉得原因可能是:由于SqlSession不是线程安全的,所以较合适的作用域是方法作用域。既然是方法作用域,也就不会多次调用getMapper方法,所以做不做缓存也就无所谓了。

执行Mapper接口方法

执行Mapper方法xxxMapper.selectById(1L);其内部执行过程相当复杂。

调用MapperProxy的invoke方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断是否是Object声明的方法,若是就直接调用
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 得到method对应的MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 由该method对象执行具体动作
return mapperMethod.execute(sqlSession, args);
}

mapperMethod是怎么来的?

先从缓存中查找有没有对应的MapperMethod对象,若没有,则新建并存入缓存。

1
2
3
4
5
6
7
8
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}

mapperMethod如何执行的

注意:以下代码并不是连续的,这里只选了相对关键的代码。

1
2
3
4
5
6
7
8
9
10
11
12
public Object execute(SqlSession sqlSession, Object[] args) {
/* 这里不贴出具体的代码,主要过程如下:
** 1. 判断当前method的SQL类型:insert、update、delete or select
** 2. 将参数转化为SQL参数
** 3. 执行
*/
...
// 因为我debug的是select具体id的记录,所以最后执行的是以下语句
result = sqlSession.selectOne(command.getName(), param);
...
return result;
}

接下来一步步debug,直到:

1
2
3
4
5
6
7
8
9
10
11
12
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// MappedStatement与具体的接口方法相关,并且MappedStatement由Configuration持有,也可以说是SqlSessionFactory持有
MappedStatement ms = configuration.getMappedStatement(statement);
// 由执行器进行查询
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();
}
}

接下来继续debug…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 之前已经说了ms由SqlSessionFactory持有
// 所以此处的cache为二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
// 若开启了二级缓存,则会先去二级缓存中查找
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 最后将结果放入二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 由于我没有开启二级缓存,所以执行该方法
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

继续debug…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 从一级缓存中查找是否命中
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 未命中,所以需要真正对数据库进行查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
...
}

往下debug n步会发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 真正的数据库连接是在该方法内创建的(实际上还嵌套了好几层)
// 创建连接也需要进行几个过程
// 1. 当前是否有空闲连接,有的话直接拿来用
// 2. 当前的活跃连接是否达到最大连接数,若没有则创建新连接
// 3. 判断是否有超时的连接,若有,则重新创建连接(内部的真正连接其实没变),只是换了个壳,另外还需要对过期连接进行回滚(如果没有设定自动提交)
// 4. 等待直到获取连接(当等待次数达到阈值时,抛出异常)
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

接下来就是根据连接进行查询了。

小结

由于这部分内容有点多,稍微进行一下总结,在执行Mapper接口方法时:

  1. 先判断是否有该方法的MapperMethod对象存在。
    1. 若没有,则创建。
  2. 在MapperMethod对象执行方法时。
    1. 参数转化等。
    2. 先查询二级缓存(若开启),命中则返回。
    3. 再查询一级缓存,命中则返回。
    4. 执行数据库操作,根据当前连接池情况获得连接,并执行操作,返回结果。

MyBatis xml模板

以下提供了一些MyBatis会使用到的xml配置文件

mybatis-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.twd.test1"/>
</typeAliases>

<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/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="twdlll"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.twd.test2.mapper"/>
</mappers>
</configuration>

mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?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="com.twd.test2.mapper.UserMapper">
<resultMap id="userMap" type="com.twd.test2.model.SysUser">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="userPassword" column="user_password"/>
<result property="userEmail" column="user_email"/>
<result property="userInfo" column="user_info"/>
<result property="headImg" column="head_img"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>

<select id="selectById" resultMap="userMap">
select * from sys_user where id = #{id}
</select>
<select id="selectAll" resultType="com.twd.test2.model.SysUser">
select * from sys_user
</select>
<insert id="insert">
insert into sys_user
values(#{id}, #{userName}, #{userPassword}, #{userEmail},
#{userInfo}, #{headImg, jdbcType=BLOB},
#{createTime, jdbcType=TIMESTAMP})
</insert>
<insert id="insertReturnId" useGeneratedKeys="true" keyProperty="id">
insert into sys_user(
user_name, user_password, user_email, user_info,
head_img, create_time
)
values(
#{userName}, #{userPassword}, #{userEmail},
#{userInfo}, #{headImg, jdbcType=BLOB},
#{createTime, jdbcType=TIMESTAMP}
)
</insert>
<select id="selectByUser" resultType="com.twd.test2.model.SysUser">
select id, user_name userName, user_password userPassword, user_email userEmail,
user_info userInfo, head_img headImg, create_time createTime
from sys_user
where 1 = 1
<if test="userName != null and userName != ''">
and user_name like concat('%', #{userName}, '%')
</if>
<if test="userEmail != null and userEmail != ''">
and user_email = #{userEmail}
</if>
</select>
</mapper>