Mybatis3源代码分析

花点时间把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

  1. 配置文件: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"/>-->
        <!--&lt;!&ndash;开启二级缓存&ndash;&gt;-->
        <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>
  1. 配置文件: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
  1. 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>
  1. 测试代码
        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。是由于存在多个分页插件产生的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容