深入mybatis源码解读~手把手带你debug分析源码

Posted 张子行的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入mybatis源码解读~手把手带你debug分析源码相关的知识,希望对你有一定的参考价值。

emmm时隔一个多月没写博客了,我终于还是没忍住对mybatis这个框架下手了哈哈哈哈。搞懂源码就是爽啊,本文大致脉络基于下图分析

在这里插入图片描述

mybatis是一款持久性的ORM框架,目的在于把数据库中的表中的信息转换成对象供我们操作,也就是说我们对数据库的操作有了mybatis可以转变为对对象的操作。

  • mybatis是怎么实现的呢?

要记到一点市面上所有的ORM框架无论如何都离不开JDBC操作,我们所谓的mybatis也好hibernate也罢其实本质都是对JDBC的包装而已。

先来回顾一下传统的JDBC操作步骤

  1. 加载驱动
  2. 获取连接
  3. 创建statement对象
  4. statement参数填充
  5. 执行查询(更新、删除)操作
  6. 从resultSet中获取结果集

在这里插入图片描述

/**
     * 使用JDBC连接并操作mysql数据库
     */
    public static void main(String[] args) {
        // 数据库驱动类名的字符串
        String driver = "com.mysql.cj.jdbc.Driver";
        // 数据库连接串
        String url = "url";
        // 用户名
        String username = "username";
        // 密码
        String password = "password";
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            // 1、加载数据库驱动( 成功加载后,会将Driver类的实例注册到DriverManager类中)
            Class.forName(driver);
            // 2、获取数据库连接
            conn = DriverManager.getConnection(url, username, password);
            // 3、获取数据库操作对象
            stmt = conn.createStatement();
            // 4、定义操作的SQL语句
            String sql = "select * from user where id = 1";
            // 5、执行数据库操作
            rs = stmt.executeQuery(sql);
            // 6、获取并操作结果集
            while (rs.next()) {
                System.out.println(rs.getInt("id"));
                System.out.println(rs.getString("name"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭对象,回收数据库资源
            if (rs != null) { //关闭结果集对象
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) { // 关闭数据库操作对象
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) { // 关闭数据库连接对象
                try {
                    if (!conn.isClosed()) {
                        conn.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

研究mybatis源码入口的选择

如果从mybatisPlus项目分析mybatis的源码写如下demo即可直接debug了,毕竟springboot+mybatisPlus项目给我简化了很多配置,很多细节可能分析不到了,个人建议把mybatis源码克隆到本地,然后进行后续的源码研究。

 	@Autowired
    UserMapper userMapper;
    @Test
    public void test5() {
        User user = userMapper.queryManyparam("zzh", 1);
        User user2 = userMapper.queryManyparam("zzh", 1);
        //俩个不同的session,导致一级缓存失效
        System.out.println(user == user2);
    }

原生mybatis操作数据库

  1. 初始化sqlSessionFactory()
  2. 利用sqlSessionFactory获取对应的mapper
  3. 利用mapper对数据库进行查询、更新、删除操作
  private Configuration configuration;
  private JdbcTransaction jdbcTransaction;
  private Connection connection;
  private Reader resourceAsReader;
  private SqlSessionFactory sqlSessionFactory;
  
  public void init() throws SQLException, IOException {
    connection = DriverManager.getConnection("url", "username", "password");
    resourceAsReader = Resources.getResourceAsReader("mybatis.xml");
    jdbcTransaction = new JdbcTransaction(connection);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
    configuration = sqlSessionFactory.getConfiguration();
  }
  /**
   * 相同的sql只会编译处理一次
   */
  @Test
  public void b() throws IOException, SQLException {
    init();
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    System.out.println(mapper.query(1));
    System.out.println(mapper.query(1));
  }

更加原生的mybatis操作数据库

  • 直接使用执行器操作数据库(simpleExecutor、cacheExecutor、reuseExecutor)
  • simpleExecutor:每次都会创建一个新的prepareStatement对象
  • cacheExecutor:针对mybatis二级缓存设计的执行器
  • reuseExecutor:相同的sql只会进行一次预处理,也就是说会进行prepareStatement的复用
  @Test
  public void a() throws SQLException, IOException {
    init();
    ReuseExecutor reuseExecutor = new ReuseExecutor(this.configuration, jdbcTransaction);
    MappedStatement mappedStatement = this.configuration.getMappedStatement("zzhTest.mybatis.mapper.UserMapper.queryManyparam");
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("id",1);
    hashMap.put("name","zzh");
    List<Object> objects1 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
    List<Object> objects2 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
    System.out.println(objects1.get(0));
    System.out.println(objects1.get(0) == objects2.get(0));
  }

预处理次数的体现:控制台 Preparing: select * from user where id = ? 出现的次数即为预处理的次数
在这里插入图片描述

原生mybatis操作数据库源码分析

废话不多说我这里直接用原生的mybatis debug源码了(其实本质都是一样的咯),如下图片中的代码
在这里插入图片描述
我们从sqlSession中获取到的mapper对象本质是一个代理对象,debug进去newInstance()看是如何创建我们的mapper代理对象的?

在这里插入图片描述

把defultSqlSession、目标对象实现的接口类、methodCache包装成一个InvocationHandler对象,继而利用jdk代理生成对应的代理对象
在这里插入图片描述

小结:通过sqlSession.getMapper(UserMapper.class)获取到的是一个通过jdk代理生成的代理对象

分析mybatis查询(select)语句执行操作

mapper中的查询语句操作如下
在这里插入图片描述

直接debug mapper.query(1)来到这里。根据对数据库不同的操作有相应的分支进行处理,由于我们是查询操作来到select分支
在这里插入图片描述

  • select分支做了什么事情?
  1. 维护好paramName与参数paramValue的关系,包装成一个map。怎么解析的?下文有讲哦

例如拿如下代码分析就是维护@Param(“id”) 中的id 与 1 的关系,此时并没有解析sql语句中的id哦

@Select("select * from user where id = #{id}")
User query(@Param("id") Integer Uid);
query(1);

在这里插入图片描述
2. 根据这个param+查询方法的全路径限定名,进行数据库的查询操作(result=sqlSession.selectOne(command.getName(), param);)。怎么查询?下文有讲哦
在这里插入图片描述

convertArgsToSqlCommandParam(参数解析过程)

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      /**
       * 遍历所有的@params()中的参数name
       */
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        /**
         * args:方法中传入的参数value数组
         * entry.getValue():@params()中的参数name
         */
        param.put(entry.getValue(), args[entry.getKey()]);
        /**
         * add generic param names (param1, param2, ...)
         */
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      /**
       * 建立paramName与paramValue的关系
       * void query(@param(id)int id,@param(name)String name)
       * query(1,"zzh")
       * 例如:id-1、name-zzh
       */
      return param;
    }
  }

看源码可能有点绕,不过我注释也写了很全哦,不懂没关系再来梳理一遍参数解析过程。mybatis中有如下俩个map

  1. args:保存的是我们的查询方法的参数value值。例如(1,2,3)
  2. names:保存的是我们查询方法的参数name值。例如(id,name,age)

由于下标都是一一对应的,通过args[names.getKey()]就可以获取到对应参数name的value值了。通过这样一个个维护一下关系,最终的param就形成了以参数name为key、以参数value为value的关系的一个map

  • 注意:mybatis显示的会加上例如param1-value这种类型的映射

在这里插入图片描述
在这里插入图片描述

selectOne(查询过程核心)

我们debug进去会来到这里,先获取对应的MappedStatement,然后走cacheExecutor执行器中的操作
在这里插入图片描述

mybatis使用的是一种装饰器的模式,在真正的查询数据库之前会依次从二级缓存(SynchronizedCache)、一级缓存(localCache)中获取数据,如果还获取不到才会走查询数据库的逻辑

点进上图的executor.query()会来到下图的cacheExecutor中的query方法,在此之前cacheExecutor还干了俩件事

  1. 将当前查询sql相关的信息包装成一个boundSql对象
  2. 创建一个cacheKey。cacheKey = statementId+sql+sql参数+cacheId

在这里插入图片描述

cacheExcutor中的核心方法query()

  • 查询二级缓存的前提
    1. 我们的项目中开启了二级缓存
    2. 我们mapper类中对应的查询方法上没有加 @Options(useCache = false)
  • 没有开启二级缓存:直接走cacheExecutor中的装饰器simpleExecutor.query(),
  • 开启了正常的二级缓存相关的配置:会先判断是否需要清空二级缓存,然后从缓存管理器中的二级缓存中获取缓存,获取不到的话才会查询数据库。
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    /**
     * 获取二级缓存:SynchronizedCache
     */
    Cache cache = ms.getCache();
    if (cache != null) {
      /**
       * 尝试清空二级缓存(标记 clearOnCommit = true)
       */
      flushCacheIfRequired(ms);
      /**
       * 可以设置 @Options(useCache = false) 关闭查询二级缓存
       * resultHandler = Executor.NO_RESULT_HANDLER = null
       */
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        /**
         * 从TransactionalCacheManager中获取二级缓存中的数据
         */
          List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          /**
           * 二级缓存中获取不到数据,进行查询数据库操作
           * delegate:SimpleExecutor
           */
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          /**
           * 把数据填充进暂存区中的entriesToAddOnCommit的这个map里面
           * 暂存区:TransactionalCache
           */
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

从上面这段代码可以知道我们平常使用如下配置时的生效时机以及在mybatis中的源码体现对应在哪

  • 如何配置开启二级缓存?

在mapper类上加@CacheNamespace或者在mapper对应的xml文件中加 < cache></ cache>

  • 如何禁用二级缓存?
  @Select("select * from user where id = #{id}")
  @Options(useCache = false)
  User query(@Param("id") Integer Uid);
  • 如何每次查询都清空二级缓存(SynchronizedCache)?
  @Options(flushCache = Options.FlushCachePolicy.TRUE)
  @Select("select * from user where id = #{id}")
  User query(@Param("id") Integer Uid);

配置在源码中的体现

mybatis清空二级缓存的流程时设置 clearOnCommit = true 并且清空entriesToAddOnCommit 这个map

private final Map<Object, Object> entriesToAddOnCommit;

private void flushCacheIfRequired(MappedStatement ms) {
	/**
     * 获取二级缓存
     */
    Cache cache = ms.getCache();
    /**
     * 如果开启对应的方法上面开启了 FlushCachePolicy = true
     * 那么此处会清除二级缓存(本质是标记 clearOnCommit = true)
     */
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

 @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

怎么从二级缓存中获取数据?TODO

接着分析cacheExecutor.query()中的tcm.getObject()这行代码可以知道怎么从二级缓存中获取数据的哦。

  1. 首先从以SynchronizedCache为头的Cache链条依次getObject(cacheKey)获取缓存数据
  2. 然后判断是否缓存中有cacheKey对应的缓存,如果没有,将此key存入一个名字为entriesMissedInCache的map中,目的就是为了防止缓存穿透。相当于把这个人拉入黑名单了。emm可以看我以前写的 缓存问题系列文章
  3. 如果拿到了缓存,还有一个判断是否clearOnCommit被标记为true?这个是为了解决缓存一致性问题的。可以看我以前写的文章简单解决缓存+数据库数据一致性问题
  4. 最终才是返回从缓存中获取到的数据
public Object getObject(Object key) {
    /**
     * delegate:Cache(以SynchronizedCache为头的Cache链条)
     * key:statementId~sql~参数~id组成
     */
    Object object = delegate.getObject(key);
    /**
     *
     * 防止缓存穿透:二级缓存中没有数据,将key放入entriesMissedInCache(HashSet)中
     * 
     */
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    /**
     * 如果此时有人清空二级缓存那么clearOnCommit = true
     * 此时的效果也相当于从二级缓存中获取不到数据
     */
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

如果是我们第一次debug到这肯定二级缓存中没有数据撒,还记得我们分析到哪了咩,分析到下图这。本质如下箭头标注的query()是调用同一个query()方法,如果没有获取到缓存将会执行箭头标注的方法(simpleExecutor.query(…))。
在这里插入图片描述

simpleExecutor中的核心方法query()

此query()大体思路分析如下:

  1. queryStack == 0 && 设置了FlushCachePolicy.TRUE那么每次都会先清空一级缓存
  2. 每次从一级缓存中获取数据前,记录查询次数(queryStack++)
  3. 查询一级缓存(localCache)
  4. 缓存中获取不到数据(list)查询数据库操作,反之不用
  5. 将延时map(deferredLoads)中的数据添加到list
  6. 设置了一级缓存的域范围为STATEMENT将会在每次查询完毕后清空一级缓存
@Override
  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.");
    }
    /**
     * 如果设置了flushCacheRequired属性将会清空缓存
     * 例如:@Options(flushCache = Options.FlushCachePolicy.TRUE)
     */
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      /**
       * 清空一级缓存中的数据
       * 一级缓存:localCache
       */
      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--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      /**
       * 清空延时加载map中的数据
       */
      deferredLoads.clear();
      /**
       * 设置了一级缓存的域范围为STATEMENT将会在每次查询完毕后清空一级缓存
       */
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

思路分析完了来扣细节~~~~~~~~~~~~

  • mybatis是如何清空一级缓存的?

清空了localOutputParameterCache、localCache(一级缓存)

以上是关于深入mybatis源码解读~手把手带你debug分析源码的主要内容,如果未能解决你的问题,请参考以下文章

手把手带你阅读Mybatis源码执行篇

知其然而知其所以然~线程池深入源码分析-手把手debug源码系列

别怕,手把手带你撕拉扯下SpringMVC的外衣

深入比特币以太坊源码带你解读POW共识机制

Mybatis源码解读-9种设计模式总结

Mybatis源码解读-设计模式总结