java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis 二级缓存机制

MyBatis 二级缓存实现机制的示例解析

作者:一名技术极客

本文主要介绍了MyBatis 二级缓存实现机制的示例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前置准备

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

@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

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

MapperProxyinvoke方法本质上是交给了MapperMethodInvokerMapperMethodInvoker实质就是封装了一层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());


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。可选的StatementHandlerSimpleStatementHandler、PreparedStatementHandlerCallableStatementHandler三种。选用哪种在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对象,这个对象中同时会 封装ParameterHandlerResultSetHandler对象。调用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流程

Sql执行流程

MyBatis一些重要的类

到此这篇关于MyBatis 二级缓存实现机制的示例解析的文章就介绍到这了,更多相关MyBatis 二级缓存机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:
阅读全文