51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

从源码彻底吃透Mybatis

MyBatis概述

# Mybatis是做什么的?
    MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
    MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML或注解来配置和映射原始类型、
    接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
  • Pom文件导入
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>x.x.x</version>
</dependency>
  • mybatis-config配置文件
<?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>

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!--实体别名-->
    <typeAliases>
        <typeAlias type="com.jorry.entity.User" alias="user"/>
    </typeAliases>


    <!--环境信息-->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/test?useUnicode=true&serverTimezone=GMT%2B8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>


    <!--Mapper映射文件-->
    <mappers>
        <!--        <package name=""/>-->
        <mapper resource="userDaoMapper.xml"/>
    </mappers>


</configuration>
  • mybatis开发步骤
# 1.entity
# 2.类型别名
# 3.table
# 4.DAO接口
# 5.Mapper文件
# 6.Mapper文件的注册
# 7.API编程

> Api测试

public class ApiTest {

  /**
   * 用于测试 Mybatis
   */
  @Test
  public void test() throws IOException {
    //第一阶段:Mybatis配置文件初始化阶段
    String resource = "mybatis-config.xml";
    //获取配置文件输入流
    InputStream is = null;
    try {
      is = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    //获取SqlSessionFactory工厂
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

    //第二阶段:数据读写阶段
    try(SqlSession sqlSession=sqlSessionFactory.openSession()){
     //根据动态代理获取接口的实现类
      UserDaoMapper userDaoMapper = sqlSession.getMapper(UserDaoMapper.class);
      //执行查询操作
      List<User> users = userDaoMapper.queryUsers();
      users.forEach(System.out::println);
    }
  }
}

Mybatis的操作主要分为两大阶段:

  • 1、Mybatis初始化阶段,该阶段主要对配置文件进行初始化,将mybatis-config.xml文件解析成Configuration对象,这个过程在Mybatis启动过程中只会启动一次。

  • 2、第二阶段:数据读写阶段,通过JDK动态代理的方式,获取接口的实现类,获取结果

一、Mybatis初始化阶段

1.1、获取InputStream

    String resource = "mybatis-config.xml";
    InputStream is = null;
    try {
      is = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  • org.apache.ibatis.io.ClassLoaderWrapper#getResourceAsStream(java.lang.String, java.lang.ClassLoader[])
  InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {

        InputStream returnValue = cl.getResourceAsStream(resource);

        if (null == returnValue) {
          returnValue = cl.getResourceAsStream("/" + resource);
        }

        if (null != returnValue) {
          return returnValue;
        }
      }
    }
    return null;
  }
# resource: 配置文件路径
# classLoader:类加载器 负责加载类的对象,一般情况下类加载器会将名称转换为文件名,然后从文件系统中读取该文件的类文件,因此,类加载器具有读取外部资源的能力

# 执行过程:调用getResourceAsStream方法会依次调用传入的每一个类加载器的getResourceAsStream方法来尝试获取配置文件的输入流,在尝试过程中,如果失败的话,会在传入的路径前面加上"/",然后再次尝试。

1.2、配置文件读取

  //获取SqlSessionFactory工厂
  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
#    这一步是创建一个SqlSessionFactoryBuilder类的实例,然后调用了其build方法
  • org.apache.ibatis.session.SqlSessionFactoryBuilder#build(InputStream, String, Properties)
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //核心代码
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } 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.
      }
    }
  }

> 整个方法中最核心部分为:

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

这两句主要进行了两步操作:

  • 1、生成了XMLConfiguration对象。并调用其Parse方法,得到了Configuration对象

  • 2、调用了SqlSessionFactoryBuilder自身的build方法,传入参数为上一步得到的Configuration对象

首先对XMLConfiguration.parse()进行详解:

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //解析mybatis-config.xml的内容
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
# 以上代码  parseConfiguration(parser.evalNode("/configuration")); 表示解析mybatis-config.xml配置文件的根节点,这里是解析整个配置文件的入口
  • parseConfiguration()
  private void parseConfiguration(XNode root) {
    try {
      //解析 properties 标签
      propertiesElement(root.evalNode("properties"));
      //解析setting标签
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      //解析typeAliases标签 类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析plugins标签
      pluginElement(root.evalNode("plugins"));
      //解析objectFactory标签
      objectFactoryElement(root.evalNode("objectFactory"));
      //解析objectWrapperFactory标签
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析reflectorFactory标签
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      //解析environments标签
      environmentsElement(root.evalNode("environments"));
      //解析databaseIdProvider标签
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //解析typeHandlers标签
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析 mappers标签
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  • 主要解析以下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></properties>
    
    <settings>
        <setting name="" value=""/>
    </settings>
    
    <!--实体别名-->
    <typeAliases>
        <typeAlias type="com.jorry.entity.User" alias="user"/>
    </typeAliases>

    <plugins>
        <plugin interceptor=""></plugin>
    </plugins>
    
    <objectFactory type=""></objectFactory>
    
    <objectWrapperFactory type=""/>
    
    <reflectorFactory type=""/>
    

    <!--环境信息-->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/test?useUnicode=true&serverTimezone=GMT%2B8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <databaseIdProvider type=""></databaseIdProvider>
    
    <typeHandlers></typeHandlers>
    
    

    <!--Mapper映射文件-->
    <mappers>
        <mapper resource="userDaoMapper.xml"/>
    </mappers>


</configuration>
# 进入每一个子方法中,解析出的相关信息都放到了Configuration对象中,因此Configuration中保存了配置文件的所有设置信息,也保存了映射文件的信息。

二、数据读写阶段

2.1、获取SqlSession对象

# 在初始化阶段结束之后,我们来对读写阶段进行追踪,初步探索当进行一次数据库的读与写操作时,Mybatis需要进行哪些操作??
SqlSession sqlSession = sqlSessionFactory.openSession()
  • 在 DefaultSqlSessionFactory 中找到了openSessionFromDataSource方法,这是生成 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);
      //获取Executor,(Mybatis二级缓存就在这个方法里面实现的)
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }
	我们看到 Configuration对象中存储的设置信息被用来创建各种对象,包括事务工厂 TransactionFactory、执行器 Executor及默认的 DefaultSqlSession。
	进入 DefaultSqlSession 类,可以看到它提供了查询、增加、更新、删除、提交、回滚等大量的方法。从 DefaultSqlSession 返回后,主方法中"SqlSession session=sqlSessionFactory.openSession()"这句代码就执行完毕了。
	有一点需要注意,数据读写阶段是在进行数据读写时触发的,但并不是每次读写都会触发"SqlSession session=sqlSessionFactory.openSession()"操作,因为该操作得到的SqlSession对象可以供多次数据库读写操作复用。

2.2、映射接口文件与映射文件的绑定

# 映射接口文件指:UserDaoMapper.java
# 映射文件指:UserDaoMapper.xml等存有操作Sql语句的文件,最终Mybatis将映射接口文件与映射文件一一对应起来
//根据接口获取对应接口的实现类
UserDaoMapper userDaoMapper = sqlSession.getMapper(UserDaoMapper.class);
# 该操作通过调用Configuration中的getMapper方法转接,最终进入MapperRegistry类中的getMapper方法
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   //根据类型获取MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
     //动态代理获取接口实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

2.3、映射接口的代理

我们从以上分析可知:经过 mapperProxyFactory.newInstance(sqlSession);
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  • 由次可以看出,这是一个基于动态代理的,mapperProxy的invoke方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //判断是Object类的方法,放行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      }
      //接口代理方法执行
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        if (!m.isDefault()) {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          }
          return new DefaultMethodInvoker(getMethodHandleJava9(method));
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException
            | NoSuchMethodException e) {
          throw new RuntimeException(e);
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }
  • 然后会触发MapperMethod对象execute方法
  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()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          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;
  }

2.4、Sql语句的查找

  • DefaultSqlSession类中的 selectList方法,该方法的源码如下
  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      dirty |= ms.isDirtySelect();
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
# 每个MappedStatement对象对应了我们设置了一个数据库操作节点,她主要定义了数据库的操作语句,输入、输出参数等信息

2.5、查询结果缓存

# 对应的数据库操作节点被查找到后,MyBatis 使用执行器开始执行语句。可以看到代码触发操作。
executor.query(ms, wrapCollection(parameter), rowBounds, handler);

  • 上述 query方法实际是一个 Executor接口中的抽象方法

# 该抽象类有两个实现,分别是:BaseExecutor和CachingExecutor类中,在抽象类上打断点,可以看到真正的实现类是CachingExecutor
# BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句
# CacheKey是为该次查询操作计算出来的缓存键。 
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
      throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  • 接下来的流程
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
# MyBatis查看当前的查询操作是否命中缓存。如果是,则从缓存中获取数据结果;否则,便通过 delegate调用 query方法。

2.6、数据库查询

  • delegate调用的 query方法再次调用了一个 Executor接口中的抽象方法

# 可以知道delegate调用的是抽象方法,我们在抽象类中打上断点,追踪程序的实际流向,我们发现,程序最终停留在BaseExecutor中的query方法中
 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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      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();
      }
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);表示开始进行查询数据库的操作
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        //执行查询操作
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
     //将查询结果放置一级缓存中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
# MyBatis先在缓存中放置一个占位符,然后调用 doQuery方法实际执行查询操作。最后,又把缓存中的占位符替换成真正的查询结果。doQuery方法是 BaseExecutor类中的抽象方法,实际运行的最终实现
  • org.apache.ibatis.executor.SimpleExecutor#doQuery
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
          boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
       //执行查询操作
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }
# 这里 ps.execute()真正执行了 SQL 语句,然后把执行结果交给 ResultHandler 对象处理。而PreparedStatement类并不是MyBatis中的类,因而ps.execute()的执行不再由MyBatis负责,而是由 com.mysql.cj.jdbc包中的类负责,

2.7、处理结果集

# 查询得到的结果并没有直接返回,而是交给ResultHandler对象进行处理,ResultHandler是结果处理器,用来接收此次查询结果的方法是该接口中的抽象方法handleResultSets
 <E> List<E> handleResultSets(Statement stmt) throws SQLException;
  • 最终实际执行的方法是 DefaultResultSetHandler中代码
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

> 总结

在整个数据库操作阶段,MyBatis完成的工作可以概述为以下几条。

  • 建立连接数据库的 SqlSession。

  • 查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现 。

  • 接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作。

  • 对数据库操作节点中的数据库操作语句进行多次处理,最终得到标准的 SQL语句。

  • 尝试从缓存中查找操作结果,如果找到则返回;如果找不到则继续从数据库中查询 。 · 从数据库中查询结果。

  • 处理结果集。

    • 建立输出对象;

    • 根据输出结果对输出对象的属性赋值。

    • 在缓存中记录查询结果。

    • 返回查询结果。

# PerpetualCache 默认实现
 装饰器:
    FifoCache 先入先出的Cache
    LruCache  最少使用
    LoggingCache Cache增加日志功能
    BlockingCache 保证一个时间只有一个线程到缓存中,查找对应的Key的数据
    ScheduledCache 能够自动刷新缓存
    SerializedCache 会自动的进行序列化与反序列化
    TransactionalCache 只有在事务操作成功时,才会将对应的数据放置在缓存中
赞(7)
未经允许不得转载:工具盒子 » 从源码彻底吃透Mybatis