ORM - MyBatis 二级缓存实现机制详解


前置准备

既然要看MyBatis源码,当然是把源码拉取下来debug一步一步看才方便呢,这里已经拉取下来,并准备好例子了。

    /**
     * @author ikcross
     * @date 2022/5/9 17:31
     */
    @Slf4j
    public class Main {
      public static void main(String[] args) {
        String resource = "org/apache/ibatis/yyqtest/resource/mybatis-config.xml";
        InputStream inputStream = null;
        try {
          // 将XML配置文件构建为Configuration配置类
          inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        // 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
        SqlSessionFactory sqlSessionFactory = null;
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = null;
        try {
          sqlSession = sqlSessionFactory.openSession();
          RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
          //Role role = roleMapper.testManyParam(1L, "张三");
          //Role role = roleMapper.testAnnotation(1L);
          Role role = roleMapper.testDynamicParam(1L, "");
    
          log.info(role.getId() + ":" + role.getRoleName() + ":" + role.getNote());
          sqlSession.commit();
    
        } catch (Exception e) {
          // TODO Auto-generated catch block
          sqlSession.rollback();
          e.printStackTrace();
        } finally {
          sqlSession.close();
        }
      }
    }

从上诉例子来看,我们观察到首先是解析配置文件,再获取SqlSession,获取到Sqlsession之后在进行各种的CRUD操作,我们先来看下SqlSession是怎么获取的。

SqlSession与SqlSessionFactory

图

​我们根据时序图,一步一步解开SqlSession的执行流程,首先来是通过SqlSessionFactoryBuilder解析配置文件,再根据配置文件构建并返回一个DefaultSqlSessionFactory,源码如下:

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      try {
        // 2. 创建XMLConfigBuilder对象用来解析XML配置文件,生成Configuration对象
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 3. 将XML配置文件内的信息解析成Java对象Configuration对象
        Configuration config = parser.parse();
        // 4. 根据Configuration对象创建出SqlSessionFactory对象
        return build(config);
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
      } finally {
        ErrorContext.instance().reset();
        try {
          if (inputStream != null) {
            inputStream.close();
          }
        } catch (IOException e) {
          // Intentionally ignore. Prefer previous error.
        }
      }
    }
    
    public SqlSessionFactory build(Configuration config) {
      return new DefaultSqlSessionFactory(config);
    }

拿到SqlSessionFactory之后,就可以根据这个SqlSessionFactory拿到SqlSession对象,源码如下:

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      Transaction tx = null;
      try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // 通过事务工厂从一个数据源来产生一个事务
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 获取执行器,这边获得的执行器已经代理拦截器的功能
        final Executor executor = configuration.newExecutor(tx, execType);
        // 获取执行器后,再将configuration和executor封装成DefaultSqlSession
        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();
      }
    }

通过上面两个步骤拿到SqlSession之后,就可以进行CRUD了

  • 将xml配置文件解析成流
  • SqlSessionFactory.build将配置文件解析成Configuration,并返回一个SqlSessionFactory对象
  • 拿到SqlSessionFactory对象之后,从Environment中拿到DataSource并产生一个事务
  • 根据事务Transaction和执行器类型(SIMPLE, REUSE, BATCH,默认是SIMPLE)获取一个执行器Executor(–>该对象非常重要,事实上sqlsession的所有操作都是通过它完成的)
  • 创建sqlsession对象。

MapperProxy

拿到Sqlsession对象之后,就可以拿到我们所写的xxxMapper,再根据这个xxxmapper就可以调用方法,最终进行CRUD。我们来关注下我们这个xxxMapper是一个接口,接口是不能被实例化的,为什么可以直接通过getMapper方式拿到呢?xxxMapper和我们的xxxMapper.xml文件之间又是如何联系的?别急,我们接着往下看。

我们先来看下时序图:

图

通过这个时序图可以看到是通过代理的方式来搞定的,当执行到自己写的方法里面的时候,其实通过了MapperProxy在进行代理。话不多说,我们直接来看下怎么获取MapperProxy对象的吧: 哦吼↑ ↓

DefaultSqlSession

    
    /**
    * 什么都不做,直接去configuration里面去找
    * @param type Mapper interface class
    * @param <T>
    * @return
    */
    @Override
    public <T> T getMapper(Class<T> type) {
    	return configuration.getMapper(type, this);
    }

Configuration

    // mapperRegistry实质上是一个Map,里面注册了启动过程中解析的各种Mapper.xml
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
    /**
     * configuration也什么都不做,去mapperRegistry里面找
     * @param type
     * @param sqlSession
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    	return mapperRegistry.getMapper(type, sqlSession);
    }

这里会发现,这个mapperRegistry是什么东西啊,mapperRegistry实质上是一个Map,里面注册了启动过程中解析的各种Mapper.xml。我们点进去看看

    Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

​mapperRegistry的key是接口的Class类型

​mapperRegistry的Value是MapperProxyFactory,用于生成对应的MapperProxy(动态代理类)

​这里具体是怎么加进去的可以去了解了解解析配置的篇章,我们接着往下走,看看MapperRegistry的源码:

MapperRegistry

    /**
     * MapperRegistry标识很无奈,没人做,只有我来做了
     * @param type
     * @param sqlSession
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      // MapperRegistry表示也要偷懒,偷偷的把粗活交给MapperProxyFactory去做。
      final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
      // 如果配置文件中没有配置相关Mapper,直接抛异常
      if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
      }
      try {
        // 我们看到mapper都是接口,因为接口是不能实例化的,这里通过动态代理实现,具体可以看下MapperProxy这个类
        return mapperProxyFactory.newInstance(sqlSession);
      } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
      }
    }

MapperProxyFactory这里是将接口实例化的过程,我们进去看看:

MapperProxyFactory

    public class MapperProxyFactory<T> {
      /**
       * 生成Mapper接口的动态代理类MapperProxy,MapperProxy实现了InvocationHandler接口
       *
       * @param mapperProxy
       * @return
       */
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
          // 动态代理,我们写的一些接口
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
      }
    
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    }

​通过这个MapperProxyFactory就可以拿到类MapperProxy代理对象,简单来说,我们通过这个代理就可以很方便的去使用我们写的一些dao层的接口了,上例子

      RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
      Role role = roleMapper.testManyParam(1L, "张三");
      Role role = roleMapper.testAnnotation(1L);
      Role role = roleMapper.testDynamicParam(1L, "");

拿到这个对象之后,接下来就是sql语句的执行了呢,来,上源码,真正的主菜来了。

Executor

​ SqlSession只是一个门面,前面做的一些事情都只是铺垫,真正的的干事的其实是Excutor,SqlSession对数据库的操作都是通过Executor来完成的。与SqlSession一样,Executor也是动态创建的,老样子先上时序图:

图

在看MapperProxy的方法之前,我们先回到获取Excutor对象的方法configuration.newExecutor(tx, execType);

    /**
     * Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。
     *
     * @param transaction
     * @param executorType
     * @return
     */
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
      executorType = executorType == null ? defaultExecutorType : executorType;
      // 这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
      executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
      Executor executor;
      // 普通Executor又分为三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
      if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
      } else if (ExecutorType.REUSE == executorType) {
        // 执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String,Statement>内,供下一次使用。简言之,就是重复使用Statement对象
        executor = new ReuseExecutor(this, transaction);
      } else {
        // 每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
        executor = new SimpleExecutor(this, transaction);
      }
      if (cacheEnabled) {
        // CacheExecutor其实是封装了普通的Executor,和普通的区别是在查询前先会查询缓存中
        // 是否存在结果,如果存在就使用缓存中的结果,如果不存在还是使用普通的Executor进行
        // 查询,再将查询出来的结果存入缓存。
        executor = new CachingExecutor(executor);
      }
      // 调用每个拦截器的方法,这里的话应该是为了方便扩展,方便开发者自定义拦截器做一些处理
      executor = (Executor) interceptorChain.pluginAll(executor);
      return executor;
    }

从上诉代码可以看出,如果不开启缓存cache的话,创建的Executor只是3中基础类型之一,BatchExecutor专门用于执行批量sql操作,ReuseExecutor会重用statement执行sql操作,SimpleExecutor只是简单执行sql没有什么特别的。

​开启了缓存(默认开启),就会去创建CachingExecutor,这个缓存执行器在查询数据库前会先查找缓存是否存在结果,如果存在就使用缓存中的结果,如果不存在还是使用普通的Executor进行查询,再将查询出来的结果存入缓存。我们还看到这里有个加载插件的方法,就说明这里是可以被插件拦截的,如果定义了针对Executor类型的插件,最终生成的Executor对象是被各个插件插入后的代理对象。

​接下来,咱们才要真正的去了解sql的执行过程了。回到上面来看,我们拿到了MapperProxy,调用Mapper接口的所有方法都会优先调用到这个代理类的invoke方法(注意由于MyBatis中的Mapper接口没有实现类,所以MpperProxy这个代理对象中没有委托类,也就是说MapperProxy干了代理类和委托类的事情),具体是怎么做的,上源码:

MapperProxy

MapperProxy的invoke方法本质上是交给了MapperMethodInvoker,MapperMethodInvoker实质就是封装了一层MapperMethod。

    /**
     * 通过动态代理后,所有的Mapper方法调用都会走这个invoke方法
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
        // 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
        if (Object.class.equals(method.getDeclaringClass())) {
          return method.invoke(this, args);
        } else {
          // cachedInvoker封装了一个MapperMethod对象
          // 拆分出来更好理解
          MapperMethodInvoker mapperMethodInvoker = cachedInvoker(method);
          return mapperMethodInvoker.invoke(proxy, method, args, sqlSession);
        }
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }

MapperMethod

根据参数和返回值类型选择不同的sqlSession来执行。这样mapper对象和sqlSession就真正关联起来了

    /**
     * 这里其实就是先判断CRUD的类型,根据类型去选择到底执行了sqlSession中的哪个方法
     * @param sqlSession
     * @param args
     * @return
     */
    public Object execute(SqlSession sqlSession, Object[] args) {
      Object result;
      switch (command.getType()) {
        case INSERT: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
          break;
        }
        case UPDATE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
          break;
        }
        case DELETE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
          break;
        }
        case SELECT:
          if (method.returnsVoid() && method.hasResultHandler()) {
            executeWithResultHandler(sqlSession, args);
            result = null;
          } else if (method.returnsMany()) {
            // 多条记录
            result = executeForMany(sqlSession, args);
          } else if (method.returnsMap()) {
            // 如果结果是map
            result = executeForMap(sqlSession, args);
          } else if (method.returnsCursor()) {
            result = executeForCursor(sqlSession, args);
          } else {
            // 从字面意义上看,讲转换的参数放到sql里面
            // Map<String, Object> param
            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;
        case FLUSH:
          result = sqlSession.flushStatements();
          break;
        default:
          throw new BindingException("Unknown execution method for: " + command.getName());
      }
      if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName()
            + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
      }
      return result;
    }

既然又回到SqlSession了,前面提到过,sqlsession只是一个门面,真正发挥作用的是executor,对sqlsession方法的访问最终都会落到executor的相应方法上去。Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。Executor的创建前面已经介绍了,那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法。这里有宝宝就好奇了,从上面的代码来看只有selectOne方法呢,哪里来的selectList。其实selectOne方法里面本质就是调用了selectList方法。

    @Override
    public <T> T selectOne(String statement, Object parameter) {
      // Popular vote was to return null on 0 results and throw exception on too many.
      // 转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
      // 这里没有查询到结果的时候会返回null。因此一般建议mapper中编写resultType使用包装类型
      List<T> list = this.selectList(statement, parameter);
      if (list.size() == 1) {
        return list.get(0);
      } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
      } else {
        return null;
      }
    }

石锤了!!!,接下来我们来看下selectList方法。

DefaultSqlSession

    /**
     *
     * @param statement 全限定名称+方法名 例如org.apache.ibatis.yyqtest.mapper.RoleMapper.testAnnotation
     * @param parameter
     * @param rowBounds
     * @param handler
     * @param <E>
     * @return
     */
    private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
      try {
        // 根据传入的全限定命+方法名从映射的map中取出MappedStatement对象
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 之类能看到CRUD实际上是交给Executor处理
        return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }

​这里的statement是全限定名称+方法名字,例如org.apache.ibatis.yyqtest.mapper.RoleMapper.testAnnotation,具体怎么来的,可以自己debug看下是怎么得到的。这里的getMapperStatement做了什么呢。话不多说上源码。

     protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
        .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    
    Configuration
    public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
      if (validateIncompleteStatements) {
        buildAllStatements();
      }
      return mappedStatements.get(id);
    }

​映射的语句,存在Map里,这里应该是扫描配置文件的时候就已经加进去了 key是全限定名+方法名 value是对应的mappedStatements。这个MapperStatements方法到底有什么作用呢,我们先别急。先回到executor.query的方法继续往下走。

StatementHandler

可以看出,Executor本质上也是个甩手掌柜,具体的事情原来是StatementHandler来完成的。当Executor将指挥棒交给StatementHandler后,接下来的工作就是StatementHandler的事了。我们先看看StatementHandler是如何创建的:

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
      StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
      statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
      return statementHandler;
    }

可以看到每次创建的StatementHandler都是RoutingStatementHandler,它只是一个分发者,他一个属性delegate用于指定用哪种具体的StatementHandler。可选的StatementHandler有SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler三种。选用哪种在mapper配置文件的每个statement里指定,默认的是PreparedStatementHandler。同时还要注意到StatementHandler是可以被拦截器拦截的,和Executor一样,被拦截器拦截后的对像是一个代理对象。

tatementHandler创建后需要执行一些初始操作,比如statement的开启和参数设置、对于PreparedStatement还需要执行参数的设置操作等。代码如下:

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      return stmt;
    }

这里呢会创建一个StatementHandler对象,这个对象中同时会 封装ParameterHandler和ResultSetHandler对象。调用StatementHandler预编译参数 以及设置参数值,使用ParameterHandler来给sql设置参数。

参数设置完毕后,执行数据库操作(update或query)。如果是query最后还有个查询结果的处理过程。接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
      PreparedStatement ps = (PreparedStatement) statement;
      ps.execute();
      // 将处理好的结果交给了ResultSetHandler
      return resultSetHandler.handleResultSets(ps);
    }

到这里,一次sql的执行流程就执行完了!

简单总结

获取SqlSession流程

  • SqlSessuibFactoryBuilder解析配置文件,并将这些配置放到一个Configuration对象,这个对象中包含了MyBatis需要的所有配置,然后会用这个Configuration对象再去创建一个SqlSessionFactory对象。
  • 拿到SqlSessionFactory对象后,会调用SqlSessionFactory的openSession方法,这个方法根据解析配置文件的事务和执行器类型来创建一个执行器,这个执行器就是真正sql的执行者。最后将执行器和configuration封装起来返回一个DefaultSqlSession
  • 拿到SqlSession之后就能执行各种CRUD的方法了。

Sql执行流程

  • 拿到SqlSession之后调用getMapper方法,拿到Maper接口的代理对象MapperProxy,调用Mapper接口的所有方法都会调用MapperProxy的invoke方法。
  • 到达invoke方法,就会掉用执行器的executer方法。
  • 往下,层层调下来会进入Executor组件(如果配置插件会对Executor进行动态代 理)的query方法,这个方法中会创建一个StatementHandler对象,这个对象中同时会 封装ParameterHandler和ResultSetHandler对象。调用StatementHandler预编译参数 以及设置参数值,使用ParameterHandler来给sql设置参数。
  • 调用StatementHandler的增删改查方法获得结果,ResultSetHandler对结果进行封 装转换,请求结束。

MyBatis一些重要的类

  • MapperRegistry:本质上是一个Map其中的key是Mapper接口的全限定名, value的MapperProxyFactory;
  • MapperProxyFactory:这个类是MapperRegistry中存的value值,在通过 sqlSession获取Mapper时,其实先获取到的是这个工厂,然后通过这个工厂创建 Mapper的动态代理类;
  • MapperProxy:实现了InvocationHandler接口,Mapper的动态代理接口方法的调 用都会到达这个类的invoke方法;
  • MapperMethod:判断你当前执行的方式是增删改查哪一种,并通过SqlSession执 行相应的操作;
  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必 要数据库增删改查功能;
  • Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓 存的维护;
  • StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设 置参数、将Statement结果集转换成List集合。
  • ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数
  • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  • TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
  • MappedStatement:MappedStatement维护了一条节点 的封装
  • SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到 BoundSql对象中,并返回
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息
  • Configuration:MyBatis所有的配置信息都维持在Configuration对象之中。

引用资料