1. 传统方式的源码剖析

1.1 初始化过程

  1. // 1.读取配置文件,加载为字节输入流(还没解析)
  2. InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
  3. public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
  4. InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
  5. if (in == null) {
  6. throw new IOException("Could not find resource " + resource);
  7. }
  8. return in;
  9. }
  10. // 2.解析配置文件,封装成Configuration对象;创建DefaultSqlSessionFactory对象
  11. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
  12. // 2.1 XML解析,创建DefaultSqlSessionFactory对象
  13. public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  14. try {
  15. // XMLConfigBuilder是专门解析Mybatis配置文件的类
  16. XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  17. // 执行XML解析,创建DefaultSqlSessionFactory对象
  18. return build(parser.parse());
  19. } catch (Exception e) {
  20. throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  21. } finally {
  22. ErrorContext.instance().reset();
  23. try {
  24. inputStream.close();
  25. } catch (IOException e) {
  26. // Intentionally ignore. Prefer previous error.
  27. }
  28. }
  29. }
  30. // 2.2 parser.parse解析
  31. public Configuration parse() {
  32. // 如果不是第一次解析,则抛出只能解析一次异常
  33. if (parsed) {
  34. throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  35. }
  36. parsed = true;
  37. // 解析XML的configuration节点(Mybatis配置文件的根节点)
  38. parseConfiguration(parser.evalNode("/configuration"));
  39. return configuration;
  40. }
  41. // 2.3 具体的解析过程
  42. private void parseConfiguration(XNode root) {
  43. try {
  44. //issue #117 read properties first
  45. // 解析<properties/>标签
  46. propertiesElement(root.evalNode("properties"));
  47. // 解析<settings/>标签
  48. Properties settings = settingsAsProperties(root.evalNode("settings"));
  49. loadCustomVfs(settings);
  50. // 解析<typeAliases/>标签
  51. typeAliasesElement(root.evalNode("typeAliases"));
  52. // 解析<plugins/>标签
  53. pluginElement(root.evalNode("plugins"));
  54. objectFactoryElement(root.evalNode("objectFactory"));
  55. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  56. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  57. settingsElement(settings);
  58. // read it after objectFactory and objectWrapperFactory issue #631
  59. environmentsElement(root.evalNode("environments"));
  60. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  61. typeHandlerElement(root.evalNode("typeHandlers"));
  62. mapperElement(root.evalNode("mappers"));
  63. } catch (Exception e) {
  64. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  65. }
  66. }
  67. public SqlSessionFactory build(Configuration config) {
  68. // 构建者设计模式
  69. return new DefaultSqlSessionFactory(config);
  70. }

1.2 sql执行流程

  1. SqlSession介绍

    1. SqlSession是⼀个接⼝,它有两个实现类:DefaultSqlSession (默认)和SqlSessionManager (弃⽤,不做介绍)。
    2. SqlSession是MyBatis中⽤于和数据库交互的顶层类,通常将它与ThreadLocal绑定,⼀个会话使⽤⼀个SqlSession,并且在使⽤完毕后需要close

      1. public class DefaultSqlSession implements SqlSession {
      2. private final Configuration configuration;
      3. private final Executor executor;
      4. }
    3. SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执⾏器

  2. 通过sqlsessionFactory生产了DefaultSqlSession实例对象,设置了事务不自动提交,完成了executor的创建 ```java SqlSession sqlSession = sqlSessionFactory.openSession();

@Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }

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 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(); } }

  1. 3. 根据statementIdconfiguration中获取到获取到MappedStatement对象对象,然后将查询交给executor执行器
  2. ```java
  3. List<User> users = sqlSession.selectList("com.lagou.entity.User.findAll");
  4. @Override
  5. public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  6. try {
  7. // 获取到MappedStatement对象
  8. MappedStatement ms = configuration.getMappedStatement(statement);
  9. // 执行查询
  10. return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  11. } catch (Exception e) {
  12. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
  13. } finally {
  14. ErrorContext.instance().reset();
  15. }
  16. }
  1. executor执行器查询,创建一级缓存的cacheKey,判断缓存是否存在,不存在则像数据库查询 ```java @Override public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 根据传入的参数动态获得sql语句,封装在BoundSql中 BoundSql boundSql = ms.getBoundSql(parameter); // 为本次查询创建缓存的key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }

@Override public List 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 list; try { queryStack++; // 先看缓存中有没有要查询的数据 list = resultHandler == null ? (List) 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(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }

  1. 5. executor执行器像数据库查询
  2. ```java
  3. private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  4. List<E> list;
  5. // 缓存占位符
  6. localCache.putObject(key, EXECUTION_PLACEHOLDER);
  7. try {
  8. list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  9. } finally {
  10. localCache.removeObject(key);
  11. }
  12. // 把查询出来的数据加入一级缓存中
  13. localCache.putObject(key, list);
  14. if (ms.getStatementType() == StatementType.CALLABLE) {
  15. localOutputParameterCache.putObject(key, parameter);
  16. }
  17. return list;
  18. }
  1. 真正向数据库查询,执行jdbc的组件StatementHandler

    1. @Override
    2. public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    3. Statement stmt = null;
    4. try {
    5. Configuration configuration = ms.getConfiguration();
    6. StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    7. stmt = prepareStatement(handler, ms.getStatementLog());
    8. return handler.<E>query(stmt, resultHandler);
    9. } finally {
    10. closeStatement(stmt);
    11. }
    12. }
  2. StatementHandler通过ParameterHandler设置参数

  3. StatementHandler通过resultSetHandler进行返回结果集的处理

2. Mapper代理方式的源码剖析

2.1 getMapper

  1. 使用JDK动态代理对mapper接口产生代理对象
    1. UserDao mapper = sqlSession.getMapper(UserDao.class);
    ```java //DefaultSqlSession 中的 getMapper public T getMapper(Class type) { return configuration.getMapper(type, this); }

//configuration 中的给 getMapper public T getMapper(Class type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }

//MapperRegistry 中的 getMapper public T getMapper(Class type, SqlSession sqlSession) { //从 MapperRegistry 中的 HashMap 中拿 MapperProxyFactory final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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); } }

//MapperProxyFactory 类中的 newInstance ⽅法 public T newInstance(SqlSession sqlSession) { //创建了 JDK动态代理的Handler类 final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); //调⽤了重载⽅法 return newInstance(mapperProxy); }

//MapperProxy 类,实现了 InvocationHandler 接⼝ public class MapperProxy implements InvocationHandler, Serializable { //省略部分源码 private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; //构造,传⼊了 SqlSession,说明每个session中的代理对象的不同的! public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { }

  1. 2. 通过代理对象调用方法时,都会执行MapperProxy中的invoke方法
  2. <a name="ywXEt"></a>
  3. ## 3. 二级缓存源码剖析
  4. 1. 二级缓存的构建在一级缓存之上,收到查询请求时,先查询二级缓存,未命中再查询一级缓存,还未命中再查询数据库。二级缓存 ---》一级缓存 ----》数据库
  5. 2. sqlsession1查询完结果后,需要commit或者close结果才会被缓存到二级缓存。
  6. ```java
  7. public void test1() throws IOException {
  8. InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
  9. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
  10. SqlSession sqlSession1 = build.openSession();
  11. SqlSession sqlSession2 = build.openSession();
  12. List<User> users1 = sqlSession1.selectList("com.lagou.entity.User.findAll");
  13. System.out.println(users1);
  14. //如果不先commmit或者close那么二级缓存还没有储存结果
  15. sqlSession1.commit();
  16. List<User> users2 = sqlSession2.selectList("com.lagou.entity.User.findAll");
  17. System.out.println(users2);
  18. }
  1. 启用二级缓存(分三步走;第一步和第三步都是默认为true,所以可以只配置第二步

    1. 再sqlMapConfig.xml中开启全局二级缓存配置

      1. <settings>
      2. <setting name="cacheEnabled" value="true"/>
      3. </settings>
    2. 在Mapper中配置二级缓存标签

      1. <cache></cache>
    3. 在具体CURD标签上配置 useCache=true

      1. <select id="findById" resultType="com.lagou.pojo.User" useCache="true">
      2. select * from user where id = #{id}
      3. </select>
  2. 解析标签过程

  3. 查询过程解析:分析为什么收到查询请求时先走二级缓存再走一级缓存,两次查询之间需要commit一次二级缓存才会有数据

    1. 从sqlSession.selectList()入手

      1. Override
      2. public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      3. try {
      4. MappedStatement ms = configuration.getMappedStatement(statement);
      5. // 执行executor的query方法
      6. return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      7. } catch (Exception e) {
      8. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
      9. } finally {
      10. ErrorContext.instance().reset();
      11. }
      12. }
    2. 当开启二级缓存时,executor的实现类不再是BaseExecutor,而是CachingExecutor ```java @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建缓存的CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }

// 这个方法分析了如果二级缓存存在,新请求先查询二级缓存再查询一级缓存 @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 解析标签时会把Cache存到MappedStatement的cache属性中 Cache cache = ms.getCache(); // 如果没有配置,则这里的二级缓存为空 if (cache != null) { // 判断CURD标签中的flushCache属性是否为ture,是就刷新二级缓存 flushCacheIfRequired(ms); // 判断CURD中的userCache=”true” if (ms.isUseCache() && resultHandler == null) { // 存储过程 ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings(“unchecked”) // 从二级缓存中获取结果 List list = (List) tcm.getObject(cache, key); if (list == null) { // 如果二级缓存结果为空,则继续执行查询,这个query方法实际上是BaseExecutor的方 // 法,先从一级缓存查询,没有结果再查数据库 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); }

  1. 3. 如上,注意⼆级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共⽤⼀个缓存实例,会导致脏读问题。⾄于脏读问题,需要借助其他类来处理,也就是上⾯代码中tcm 变量对应的类型。下⾯分析⼀下。
  2. 4. 上面代码的tcm实际上是TransactionalCacheManager类,维护了CacheTransactionalCache的映射关系,TransactionalCache就是解决二级缓存的脏读问题的。TransactionalCache类维护的几个属性:
  3. ```java
  4. // 实际的二级缓存,tcm.getObject()就是查询这个
  5. private final Cache delegate;
  6. private boolean clearOnCommit;
  7. // 在事务未提交时,所有从数据库查询的结果存到此集合中,tcm.putObject()是先保存到这
  8. private final Map<Object, Object> entriesToAddOnCommit;
  9. // 在事务未提交时,当缓存未命中,Cachekey将缓存到此集合中
  10. private final Set<Object> entriesMissedInCache;
  1. TransactionalCache的查询是直接从实际二级缓存delegate查的,存储的时候存的是entriesToAddOnCommit,这也就解释了为什么两个查询之间如果事务没有提交,二级缓存是不会命中的 ```java public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } }

@Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); }

// 执行了commit那么二级缓存才真正存了刚刚查询的结果 public void commit() { if (clearOnCommit) { delegate.clear(); } // 把entriesToAddOnCommit和entriesMissedInCache的值刷入delegate flushPendingEntries(); reset(); }

  1. 6. 二级缓存查询过程总结:在CachingExecutor的执行查询中,先通过TransactionalCacheManager类来维护CacheTransactionalCache的映射关系,二级缓存的存储与获取实际是通过TransactionalCache类来进行的,里面维护了真正的二级缓存Cache,存储二级缓存的时候实际先放到TransactionalCache.entriesToAddOnCommit的一个map集合中,但是查询二级缓存的时候是直接通过TransactionalCache.delegate去查询的,所以这个二级缓存查询后,设置的缓存值没有立即生效,主要是直接存到delegate会产生脏读问题。
  2. 6. 二级缓存生效机制,只要看sqlSessioncommit()方法做了什么
  3. ```java
  4. @Override
  5. public void commit(boolean force) {
  6. try {
  7. // 主要是这句
  8. executor.commit(isCommitOrRollbackRequired(force));
  9. dirty = false;
  10. } catch (Exception e) {
  11. throw ExceptionFactory.wrapException("Error committing transaction.
  12. Cause: " + e, e);
  13. } finally {
  14. ErrorContext.instance().reset();
  15. }
  16. }
  17. // CachingExecutor.commit()
  18. @Override
  19. public void commit(boolean required) throws SQLException {
  20. delegate.commit(required);
  21. tcm.commit();// 在这⾥
  22. }
  23. // TransactionalCacheManager.commit()
  24. public void commit() {
  25. for (TransactionalCache txCache : transactionalCaches.values()) {
  26. txCache.commit();// 在这⾥
  27. }
  28. }
  29. // TransactionalCache.commit()
  30. public void commit() {
  31. if (clearOnCommit) {
  32. delegate.clear();
  33. }
  34. flushPendingEntries();//这⼀句
  35. reset();
  36. }
  37. // TransactionalCache.flushPendingEntries()
  38. private void flushPendingEntries() {
  39. for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
  40. // 在这⾥真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,⼆
  41. 级缓存才真正的⽣效
  42. delegate.putObject(entry.getKey(), entry.getValue());
  43. }
  44. for (Object entry : entriesMissedInCache) {
  45. if (!entriesToAddOnCommit.containsKey(entry)) {
  46. delegate.putObject(entry, null);
  47. }
  48. }
  49. }
  1. 二级缓存的刷新,主要看sqlSession的update方法 ```java public int update(String statement, Object parameter) { int var4; try {
    1. this.dirty = true;
    2. MappedStatement ms = this.configuration.getMappedStatement(statement);
    3. var4 = this.executor.update(ms, this.wrapCollection(parameter));
    } catch (Exception var8) {
    1. throw ExceptionFactory.wrapException("Error updating database. Cause:
    2. "
    3. + var8, var8);
    } finally {
    1. ErrorContext.instance().reset();
    } return var4; }

public int update(MappedStatement ms, Object parameterObject) throws SQLException { this.flushCacheIfRequired(ms); return this.delegate.update(ms, parameterObject); }

private void flushCacheIfRequired(MappedStatement ms) { //获取MappedStatement对应的Cache,进⾏清空 Cache cache = ms.getCache(); //SQL需设置flushCache=”true” 才会执⾏清空 if (cache != null && ms.isFlushCacheRequired()) { this.tcm.clear(cache); } }

  1. 8. MyBatis⼆级缓存只适⽤于不常进⾏增、删、改的数据,⽐如国家⾏政区省市区街道数据。⼀但数据变更,MyBatis会清空缓存。因此⼆级缓存不适⽤于经常进⾏更新的数据。
  2. 9. 总结:在⼆级缓存的设计上,MyBatis⼤量地运⽤了装饰者模式,如CachingExecutor, 以及各种Cache接⼝的装饰器。
  3. 1. ⼆级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
  4. 2. ⼆级缓存具有丰富的缓存策略。
  5. 3. ⼆级缓存可由多个装饰器,与基础缓存组合⽽成
  6. 4. ⼆级缓存⼯作由 ⼀个缓存装饰执⾏器CachingExecutor ⼀个事务型预缓存TransactionalCache完成。
  7. <a name="nS9BZ"></a>
  8. ## 4. 延迟加载源码分析
  9. <a name="S6Cnp"></a>
  10. ### 4.1 什么是延迟加载
  11. 1. 延迟加载:就是需要用到数据的时候才去加载,不需要用到的数据就不去加载,也叫懒加载。
  12. 2. 优点:先从单表查询,需要时再去关联表查询,大大提高数据库性能,因为查询单表比查询多表快的多。
  13. 3. 缺点:因为用到数据时才去数据库查询,这样在大批量数据查询时,因为查询工作也需要消耗时间,所以可能造成用户等待时间过长,用户体验下降。
  14. 4. 在多表中:
  15. 1. 一对多,多对多通常建议延迟加载。
  16. 2. 一对一,多对一通常情况采用立即加载。
  17. 5. 注意:延迟加载是采用嵌套查询实现的。
  18. <a name="M3bam"></a>
  19. ### 4.2 实现延迟加载
  20. 1. 局部延迟加载:在associationcollection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。
  21. ```xml
  22. <!-- 开启⼀对多 延迟加载 -->
  23. <resultMap id="userMap" type="user">
  24. <id column="id" property="id"></id>
  25. <result column="username" property="username"></result>
  26. <result column="password" property="password"></result>
  27. <result column="birthday" property="birthday"></result>
  28. <!--
  29. fetchType="lazy" 懒加载策略
  30. fetchType="eager" ⽴即加载策略
  31. -->
  32. <collection property="orderList" ofType="order" column="id"
  33. select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
  34. </collection>
  35. </resultMap>
  36. <select id="findAll" resultMap="userMap">
  37. SELECT * FROM `user`
  38. </select>
  1. 全局延迟加载:在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。

    1. <settings>
    2. <!--开启全局延迟加载功能-->
    3. <setting name="lazyLoadingEnabled" value="true"/>
    4. </settings>
  2. 注意:局部加载的策略优先与全局加载

    4.3 延迟加载的原理实现

  3. 它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的 getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤ a.getB().getName() ⽅法,进⼊拦截器的invoke(…) ⽅法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B对象的 SQL ,把 B 查询上来,然后调⽤ a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理。

  4. 总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。