花点时间把Mybatis3的源码阅读了一下,下面是把自己的阅读心得大概过一下,加深一下自己的印象,也希望对别人能有所帮助。在阅读源代码之前,强烈建议先阅读一下mybatis3的官方引导(不管是有没有使用):https://mybatis.org/mybatis-3/getting-started.html。Mybatis3的缓存使用的问题,是个绕不开的问题,建议阅读美团的技术博客:https://tech.meituan.com/2018/01/19/mybatis-cache.html肯定能够受益匪浅。
搭建测试环境
参考文章:https://mybatis.org/mybatis-3/getting-started.html
- 配置文件:mybatis-config.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 resource="mysql-config.properties">
</properties>
<settings>
<!--开启驼峰隐射,比如数据库字段 user_no->user_no-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--一级缓存-->
<!--<setting name="localCacheScope" value="SESSION"/>-->
<!--<setting name="localCacheScope" value="STATEMENT"/>-->
<!--<!–开启二级缓存–>-->
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<typeAlias type="com.spring.cloud.test.mybatis3.po.User" alias="User"></typeAlias>
</typeAliases>
<plugins>
<plugin interceptor="com.spring.cloud.test.mybatis3.plugin.ExamplePlugin">
<property name="age" value="100"/>
<property name="name" value="kaka"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
- 配置文件:mysql-config.properties
mysql.driver=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://10.106.11.151:3306/test?characterEncoding=UTF-8
mysql.username=root
mysql.password=my002714
- User表对应的Mapper配置文件UserMapper.xml
特别注意自动键生成的配置,selectKey 的用法,<where>表达式的作用可以代替 where 1=1的作用,还有<set>可以自动处理sql多于的逗号。使用<cache>配置二级缓存。
<?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.spring.cloud.test.mybatis3.mapper.UserMapper">
<cache></cache>
<select id="selectUser" resultType="User">
select * from user where id = #{id}
</select>
<!--自动生成自增主键-->
<!--<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">-->
<!--insert into user (user_no, name)-->
<!--values (#{userNo},#{name})-->
<!--</insert>-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
<selectKey keyProperty="userNo" resultType="java.lang.String" order="BEFORE">
SELECT LPAD(MAX(user_no) + 1, 4, '0') FROM `user`
</selectKey>
insert into user (user_no, name)
values (#{userNo},#{name})
</insert>
<select id="selectCondtions" resultType="User">
SELECT * FROM user
<where>
<if test="userNo!=null">
user_no=#{userNo}
</if>
<if test="name!=null">
AND name=#{name}
</if>
</where>
</select>
<update id="updateUser">
UPDATE user
<set>
<if test="userNo!=null">
user_no=#{userNo},
</if>
<if test="name!=null">
name=#{name}
</if>
</set>
WHERE id = #{id}
</update>
</mapper>
- 测试代码
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try(SqlSession session= sqlSessionFactory.openSession(true)){
User user= session.selectOne("com.spring.cloud.test.mybatis3.mapper.UserMapper.selectUser", 1);
System.out.println(user);
}
源码基础分析->指定statement直接操作
测试代码参考之上
1. 初始化SqlSessionFactory
# 开始构建(入口),默认生成一个DefaultSqlSessionFactory对象
SqlSessionFactoryBuilder.build
# 加载配置文件,最终返回一个Configuration对象
XMLConfigBuilder.parse->XMLConfigBuilder.parseConfiguration
# 加载插件
XMLConfigBuilder.pluginElement
# 加载mapper数据库映射文件,这里分两种情形:1.加载指定的package包下的所有mapper文件 2. 一个个加载
XMLConfigBuilder.mapperElement
# 转换mapper
XMLMapperBuilder.parse
# 加载mapper xml中的<ResultMap>
XMLMapperBuilder.parsePendingResultMaps
# 加载mapper xml中的<CacheRef>,主要用于二级缓存
XMLMapperBuilder.parsePendingCacheRefs
# 加载mapper xml中的 Statement,比如<Select>,<Update>等等
XMLMapperBuilder.parsePendingStatements
2. 初始化SqlSession
# 开始构建,记得设置autocommit为true,否则涉及到更改时,不手动提交的话会回滚
DefaultSqlSessionFactory.openSession(true)
# 从db开始构建SqlSession
DefaultSqlSessionFactory.openSessionFromDataSource
# 从jdbc构建事务,可以关注一下JdbcTransactionFactory生成JdbcTransaction事务对象
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(e);
tx = transactionFactory.newTransaction(e.getDataSource(), level, autoCommit);
# 生成执行器Executor,后期所有的操作都委托给Executor执行,可以通过SimpleExecutor类来理解
Executor executor = this.configuration.newExecutor(tx, execType);
# 生成SqlSession
new DefaultSqlSession(this.configuration, executor, autoCommit);
3. 通过Executor来执行CRUD
# 入口:DefaultSqlSession.selectOne
session.selectOne("com.spring.cloud.test.mybatis3.mapper.UserMapper.selectUser", 1);
# 通过statement(com.spring.cloud.test.mybatis3.mapper.UserMapper.selectUser)从加载后的缓存中获取MappedStatement ,然后委托Executor执行
MappedStatement e = this.configuration.getMappedStatement(statement);
var6 = this.executor.query(e, this.wrapCollection(parameter), rowBounds, handler);
# Executor执行查询等相关动作,之间会判断一级缓存
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
# 执行真正的数据库查询,以SimpleExecutor.doQuery为例,PreparedStatementHandler会执行最终的jdbc查询
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var6 = handler.update(stmt);
源码分析->使用动态代理访问
指定UserMapper DAO对象,通过DAO执行相关操作
1. 示例代码
try(SqlSession session= sqlSessionFactory.openSession(true)){
UserMapper userMapper= session.getMapper(UserMapper.class);
System.out.println(userMapper.selectUser(1));
}
public interface UserMapper {
User selectUser(int id);
void insertUser(User user);
List<User> selectCondtions(User user);
void updateUser(User user);
}
2. 获取UserMapper.class的动态代理工厂
# 入口
session.getMapper(UserMapper.class);
# 委托mapperRegistry获取UserMapper type的实例
this.mapperRegistry.getMapper(type, sqlSession);
# 获取生成UserMapper实例的代理工厂,注意这里是从knownMappers缓存中直接获取,是因为在解析mapper的时候已经填充该缓存。
MapperProxyFactory mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
# 生成UserMapper代理
MapperProxy mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
# 真正执行的是代理的PlainMethodInvoker.Invoke方法
private static class PlainMethodInvoker implements MapperProxy.MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
this.mapperMethod = mapperMethod;
}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return this.mapperMethod.execute(sqlSession, args);
}
}
3. 委托MapperMethod执行相关操作
# 通过上面的步骤,为了执行相关CRUD操作,实际上动态生成了DAO接口(UserMapper)的一个代理, 然后委托给MapperMethod.execute,分析下面的代码,最后还是根据指令类型,执行sqlsession的相关操作,这里我们看到了熟悉的sqlSession.selectOne,后续的操作就是上面的【源码基础分析->指定statement直接操作】
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(null.$SwitchMap$org$apache$ibatis$mapping$SqlCommandType[this.command.getType().ordinal()]) {
case 1:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case 2:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case 3:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case 4:
if(this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if(this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if(this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if(this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if(this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case 5:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if(result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method \'" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
使用Mybatis-plus时候分页生成的sql语句含有两个limit
参考:https://github.com/baomidou/mybatis-plus/issues/974。是由于存在多个分页插件产生的问题。