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

2022年8月1日
大约 14 分钟

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对象之中。

引用资料

  • https://blog.csdn.net/qq_48123829/article/details/125169561