3.[mybatis]的查询源码分析(执行流程、缓存、整合spring要点)

Spring整合MyBatis后一级缓存的使用 Mybatis的一级缓存是sqlSession级别的,在不进行Insert,Update操作的情况下,使用同一sqlsession下的多次相同查询会使用缓存结果。 Spring整个MyBatis后与Mybatis单独使用有所区别 Spring整合Mybatis后,通过SqlSessionTemplate创建sqlSession代理对象,默认每次查询都是创建新的sqlSession对象,查询执行结束后在finally中关闭代理sqlSession。 Spring整合Mybatis中,默认情况下一级缓存 阅读详情

目录

 

1.装饰器模式

2. sqlSession的创建(open)

2.1.newExecutor

3.selectOne分析

3.1.二级缓存

3.2.一级缓存

4.数据库查询核心分析(queryFromDatabase)

4.1.SimpleExecutor.doQuery

4.1.1.prepareStatement

4.1.2.预处理查询数据库

4.1.3.封装数据到对象

5.mybatis涉及的设计模式

6.mybatis整合spring

6.1.SqlSessionTemplate


1.装饰器模式

前面两篇笔记都在分析mybatis的全局配置文件的解析和原理,如果执行查询的时候mybatis的流程是怎么样的呢?在执行的流程中mybtis大量的使用到一种设计模式就是装饰器模式,就是定义了一个功能接口(执行器),然后又好几个子类去实现它,它将一些功能装饰在子类上,并且提供了缓存的执行器,这个执行器也是对功能接口进行装饰,并且由它来调度它的其他子类的装饰方法

Component,抽象构件

Component是一个接口或者抽象类,是定义我们最核心的对象,也可以说是最原始的对象,比如上面的肉夹馍。

ConcreteComponent,具体构件,或者基础构件

ConcreteComponent是最核心、最原始、最基本的接口或抽象类Component

的实现,可以单独用,也可将其进行装饰,比如上面的简单肉夹馍。Decorator,装饰角色一般是一个抽象类,继承自或实现Component,在它的属性里面有一个变量指

向Component抽象构件,我觉得这是装饰器最关键的地方。

ConcreteDecorator,具体装饰角色

ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,它们可以把

基础构件装饰成新的东西,比如把一个普通肉夹馍装饰成鸡蛋里脊肉夹馍

所以我们看下mybatis中的执行器的装饰器模式的一个类继承关系图:

 

是不是就是一个装饰器的模式,在创建Executor的时候,比如创建的是SimpleExecutor,那么如果缓存引擎是打开的,那么会创建一个CachingExecutor,将创建的SimpleExecutor通过构造丢到CachingExecutor中,当执行一个操作的时候先执行的是CachingExecutor中的方法,而真正当CachingExecutor中没有执行到想要的结果时,会调用BaseExecutor中的方法,从而调用到SimpleExecutor中的方法;所以这种设计模式就是相当于CachingExecutor是一个构建,而BaseExecutor是一个装饰角色,而下面的SimpleExecutor、BatchExecutor都是它的装饰类,当构建中不能满足功能时,通过装饰器去实现。

 

2. sqlSession的创建(open)

我们知道在mybatis中如果要执行数据库的CRUD操作,必须要先通过sessionFactory来打开也就是创建一个SqlSession,通过SqlSession才能进行数据库的CRUD操作,那么如果想要了解mybatis的整体流程,它是如何创建了SqlSession,这个要弄清楚,在前面的笔记中已经分析了SessionFactory是如何创建的,简单来说就是通过解析全局配置文件将所有的配置信息包括数据源信息解析出来放入Configuration的对象中,然后通过创建默认的session工厂也就是DefaultSqlSessionFactory,将创建好的configuration通过DefaultSqlSessionFactory传入构造方法创建的,那么SqlSession的创建也是一样的,通过DefaultSqlSessionFactory来创建打开的一个sqlSession,所以这里就先来看下是如何创建的SqlSession

 

@Override
public SqlSession openSession() {
  /**
   * 这里是创建一个默认的SqlSession=DefaultSqlSession
   * 这里没有去执行那些操作,这里主要是将前面创建的环境对象Environment获取到
   * 然后获取到事务工厂以及数据源信息,在根据这里传入的执行器类型获取一个执行器
   *  然后将这些获取到的信息封装到一个DefaultSqlSession的对象中,最后返回
   *  所以openSession()其实没有执行数据库或者说复杂的操作,就是
   *  将上一步配置文件解析的结果的一些对象获取到,饭后封装到一个DefaultSqlSession
   *  的对象中
   *  这里传入的执行器类型是:SIMPLE
   *  autoCommit:false(不自动提交)
   */
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

openSession中只有一个方法,openSessionFromDataSource,想都不用想,这个方法肯定就是去得到一个SqlSession的操作,所以进到这个方法里面去,在进这个方法之前,我们需要知道这个方法接受的参数,第一个是一个ExecutorType,这个是什么意思呢?这个就是前面设计模式将的,这里创建的要给sql执行器的类型,mybatis中有三种执行器,SIMPLE、REUSE 、BATCH,分别是简单的、复用的、批量的,所以这里需要默认一个,在前面的笔记中已经介绍了可以在全局配置中配置想要的sql执行器是如何配置的,这里就不赘述了,所以这里是要得到一个默认的执行器

这里获取的是一个默认的;第二个和第三个参数是和事务有关的,就是事务的隔离级别和是否自动提事务的;

现在来看下表这个方法的实现:

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);
    //通过得到的事务对象,通过configuration创建一个执行器,这个执行器就是数据库的CRUD的真正的执行者
    //这里将其创建出来
    final Executor executor = configuration.newExecutor(tx, execType);
    //最后将所有的对象封装成一个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();
  }
}

简单来说,这个方法的含义就是:

1.通过DefaultSqlSessionFactory拿到configuration对象,然后读取到环境信息;

2.通过环境信息拿到事务工厂;

3.通过事务工厂和执行器类型得到一个执行器;

4.然后创建一个新的DefaultSqlSession,将创建好的执行器和configuration封装到sqlSession中去。

5.最后返回这个DefaultSqlSession,完成了DefaultSqlSession的创建(open)。

 

2.1.newExecutor

 

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  //二级缓存开启的话,那么默认使用的是CachingExecutor,然后把创建的SimpleExecutor、BatchExecutor、ReuseExecutor作为属性传入,这是一种装饰模式
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

这个方法没有什么可说的,要注意的一个点就是cacheEnabled,开启了这个的话,那么创建的执行器会封装到缓存的执行器CachingExecutor中,这也就是装饰器模式的一部分,这个和二级缓存有关系的,二级缓存都在CachingExecutor中实现的,所以要开启二级缓存,那么cacheEnabled必须是为true才行,如果没有开启cacheEnabled,那么二级缓存是不起作用的;如果开启了cacheEnabled,没有开启二级缓存,那么在CachingExecutor中如果也会调用这里创建的执行器SimpleExecutor中的查询方法,当然是调用BaseExecutor,而SimpleExecutor的父类是BaseExecutor,所以整个装饰器模式就串起来了。

 

3.selectOne分析

3.1.二级缓存

selectOne方法就是查询单条数据的,在mybatis中,不管是查询单条数据还是查询列表数据,都会调用query方法,如果是单条的方法,那么要判断结果是否只有一条,如果是多条就要报错,所以这里只需要看selectlist方法即可

 

<select id="queryUser" resultMap="all" resultType="uu" >
 SELECT * FROM USER where  id=#{id}
</select>
/**
 * rowBounds这个参数是用来计算缓存的key使用的,但是可以自己生成
 * @param statement Unique identifier matching the statement to use.
 * @param parameter A parameter object to pass to the statement.
 * @param rowBounds  Bounds to limit object retrieval
 * @param <E>
 * @return
 */
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    //这里获取声明对象,就是mapper中每个select中的标签对象
    //这里通过select中的ID或者命名空间+select中的ID获取一个声明的对象
    //然后通过sql执行器去查询
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

代码中的configuration.getMappedStatement(statement)就是拿到在mapper配置文件中的queryUser的声明信息,包括了sql语句,参数类型,返回类型等等,拿到这个声明就可以进行一系列的操作,具体操作看后面的;

在openSession的时候就知道了执行器executor创建的是一个CachingExecutor执行器,而且是封装了SimpleExecutor执行器的,所以这里的executor.query是调用的CachingExecutor中的query方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  /**
   * 获取boudSql对象,包括ognl的解析,包括if where、foreach等的解析都在
   * getBoundSql方法里面处理,这个方法有点复杂,反正就是判断sql是动态sql
   * 还是一般的sql,如果是${},那么就是动态sql,如果是#{}就是一般的sql,解析的
   * 类都不一样,动态sql是DynamicSqlSource,一般的sql是RawSqlSource
   * 包括if、where、foreach等都是用了不同的类去解析的,mybatis对于动态sql中
   * 每一个标签都使用了不同的类去解析,比如在一个查询中
   * SELECT * FROM USER
   *      <where>
   *          <if test="id != null">
   *              id = #{id}
   *          </if>
   *          <if test="name != null">
   *              and name = #{name}
   *          </if>
   *      </where>
   *      这个where都是单独使用了一个类进行解析的,而且会自动将where后面的
   *      and删除,就是where后面的第一个条件会删除and or 等操作符
   *
   *
   */
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //创建一级缓存key,一级缓存mybatis是默认开启,没有办法关闭
  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是二级缓存,二级缓存要在mybatis中配置了开启才会使用
     * 也就是说一级缓存默认开启,不能关闭,二级缓存需要手动开启
     * 而在使用的时候先获取二级缓存,如果二级缓存有数据就从二级缓存中获取
     * 二级缓存和一级缓存只能用一个,如果二级缓存开启了就使用二级缓存,不会使用一级缓存
     *一级缓存是没有办法关闭的,是默认开启的
     * 二级缓存和一级缓存是平级的,Executor有简单、复用、批量的模式,而还有一个带缓存的
     * 也就是CachingExecutor,如果二级缓存开启的话,那么不管是复用的、批量的、还是简单的
     * 都是封装到二级缓存的CachingExecutor中,通过装饰器模式来调用数据库的方法
     * 所以开启了二级缓存,那么就会调用CachingExecutor,如果找不到数据才会去调用创建的执行器delegate
     * 我的这个mybatis版本,cacheEng是默认开启的
     * 二级缓存开启需要配合useCache使用,而只要是select的查询都会默认开启
     * 也就是说针对查询,一级和二级缓存都是默认开启的,先获取二级缓存
     * cache配合二级缓存使用,也就是说只有在mapper中配置了cache,那么
     * 你开启的二级缓存才会有效果
     * 也就是说开启了cacheEnabled=true并且在mapper中配置了cache并且在select中
     * 配置了useCache才会生效
     * Mybatis.xml:<setting name="cacheEnabled" value="true"/>
     * Mapper.xml:<cache flushInterval="1000"></cache>
     * select:useCache="true"
     * 才会生效二级缓存
     * 如果在select中 配置了flushCache="true",那么会清空二级缓存
     */
    Cache cache = ms.getCache();
    if (cache != null) {
      //这里是清空缓存,在select等标签中设置了flushCache=true的情况下会清除缓存
      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);
  }

既然说到了二级缓存,那么就要理解下这个二级缓存的一个执行原理和是怎么添加二级缓存的,二级缓存是针对于mapper的缓存,只需要在开启了cacheEnabled的情况下,然后在mapper中添加<cache />标签,就可以开启二级缓存了,但是要明白这个cache标签是在哪里解析的,二级缓存cahce是针对mapper的缓存,所以cahce的解析肯定就是在解析mapper的时候了,所以我们看下解析mapper的源码,回到解析全局配置文件的地方,找到解析mapper的位置

/**
 * 解析二级缓存
 * @param context
 */
private void cacheElement(XNode context) {
  if (context != null) {
    /**
     * 这里解析二级缓存的type,我们知道二级缓存的数据是放在硬盘上的,所以开启二级缓存存放的数据
     * 必须要是能够序列化的,这个mybatis提供了一个默认的type,但是mybatis也支持二级缓存的数据
     * 缓存到第三方缓存框架中,比如redis,但是需要你自己去实现二级缓存的缓存类,实现缓存到redis中
     */
    String type = context.getStringAttribute("type", "PERPETUAL");
    /**
     * 知道的二级缓存的type,看是佛叜别名注册表中存在,如果不存在,那么就会根据集配站的缓存
     * type通过反射得到一个calss,也就是我们自定义了二级缓存,我们可以把这个二级缓存的实现类
     * 通过别名注册到缓存中,然后在cahce中的type就可以直接定义别名也是可以使用的,
     * 如果不想通过这种方式,执行配置type为全限定名就可以了
     */
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    /**
     * 这个是缓存的淘汰机制,默认采用的是LRU,就是淘汰最少使用的
     * 和配置 type是一样的,可以配置到别名缓存中,也可以配置全限定名
     */
    String eviction = context.getStringAttribute("eviction", "LRU");
    //找到你配置的淘汰机制
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    //缓存刷新的时间间隔
    Long flushInterval = context.getLongAttribute("flushInterval");
    //缓存的大小
    Integer size = context.getIntAttribute("size");
    //缓存的可读状态
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    //通过建造者模式添加多个缓存的实现类,每个类实现不同的功能
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

在二级缓存的详细源码中,有一篇博客讲我觉得还是比较详细的,这里分享出这篇博客https://blog.youkuaiyun.com/weixin_40516653/article/details/108412796,有兴趣可以去看下;

myatis中的一级缓存是session级别的缓存,当session关闭或者提交过后,那么一级缓存就会失效了,而二级缓存是可以跨线程的缓存,也就是说不同的session,也是具有效果的,开启二级缓存:

全局配置文件中:

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

 

mapper配置文件中:

 

<cache></cache>
cache中的参数配置:
如果没有配置:mybatis会默认设置
type CDATA #IMPLIED 
eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED

二级缓存和一级缓存不能同时生效的,所以在开启了二级缓存过后,不同的SqlSession都能够访问到二级缓存的内容,但是第一次二级缓存中肯定是没有数据的,所以会去查询数据库,然后放入二级缓存,但是查询的时候是通过执行器的装饰器类去查询的,也会放入一级缓存,所以如果说第一个sqlsession没有关闭或者提交的情况下,二级缓存是不会生效的,这和事务有关系,因为第一个sqlSession没有提交的话,那么为了防止读取的缓存数据不是脏收据,所以需要第一个sqlSession提交过后,那么二级缓存才会生效;

还有一个就是二级缓存的数据是缓存到硬盘上或者第三方的软件系统中的,所以要缓存的对象必须是序列化的,否则要报错的。

比如下面的例子:

InputStream input = Resources.getResourceAsStream("MyBatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
Object one = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println(one);
sqlSession.close();
SqlSession sqlSession2 = sessionFactory.openSession();
one = sqlSession2.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println(one);

我查询两次,日志只打印了一次sql,证明只查询了一次数据库,而且第二次去查询的时候命中率是0.5,所以是启用了二级缓存的。结果已经知道了,那么我们就来分析下这个二级缓存和一级缓存,我们还是看下面的查询方法:

 

 

当开启了二级缓存过以后,cache是不为空的,比如就上面的测试代码逻辑中;第一个selectOne的时候,首先进入到if逻辑,并且第一次缓存中是没有数据的,所以这个时候要去调用

list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

这行代码的,而这行代码会进入到BaseExecutor的query中,这个是去查询数据库并且放入了一级缓存,但是查询返回过后这里又放入了二级缓存的对象中,那么是不是就很矛盾,其实不然,不要被这个逻辑绕晕了,首先要明白的是一级缓存和二级缓存不能同时存在的这句话其实有病句的,因为当第一次请求的时候缓存是没有的,那么查询会交给BaseExecutor去执行,执行成功过后 会放入一级缓存,但是这里有放入了二级缓存,其实这个时候只有一级缓存是生效的,二级缓存是没有生效的,不行你把上面的测试代码的close方法删除掉,那么永远都不会在二级缓存中找到数据,永远都是去了一级缓存中找到了数据,但是只能是同一个sqlsession的作用域内,因为一级缓存是session级别的缓存,不同的sqlSession有不同的一级缓存。那么为什么二级缓存不生效呢?原因就在于我上面说的当第一次查询的时候数据是放入到了一级缓存中,但是这个时候第一个sqlSession没有关闭或者提交,那么后面的请求怎么就知道了你放入缓存的这个sesion没有修改这缓存中的数据呢?万一你修改了,那么一级缓存和二级缓存都有这个数据,那不是就产生了脏数据了?所以这个和事务有关系,只有等你放入一级缓存的sqlSession关闭或者提交了,那么才会触发到二级缓存中去,感觉就是缓存移动一样,其实不一样的,我们看上面的代码中放入二级缓存的的代码是:tcm.putObject(cache, key, list);

这个tcm是TransactionalCacheManager,就是一个事务缓存管理器

主义这个tcm,我们看下放入的代码:

public void putObject(Cache cache, CacheKey key, Object value) {
  getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
  return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
@Override
public void putObject(Object key, Object object) {
  entriesToAddOnCommit.put(key, object);
}

注意看,这个缓存事务管理器先得到是那个缓存,Cache的类型有很多

 

 

在解析全局配置文件中解析往Configuration中添加Cache的时候就添加了这里面的很多个,具体可以去debug下,添加的时候是通过建造者模式添加的,这里就不多说了。

得到过后,这个事物管理器就往entriesToAddOnCommit这个方法中添加数据。

如果说第一次查询的那个SqlSession没有提交或者关闭的情况下,这个时候我们开启第二个session去查询,那么会调用List<E> list = (List<E>) tcm.getObject(cache, key);这个方法,而这个方法就是去缓存中获取数据的,看下源码:

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

这里获取又是从delegate中去获取的,这个delegate是一个装饰器类,就是上面图中的Cache下面的一个装饰器类,也就是它的一个实现类,这里的这个装饰器类是SynchronizedCache,很明显获取不到缓存数据,因为mybatis的设计者就是考虑到可能会读取到脏数据,所以只有放入缓存的session没有关闭或者提交的情况下是不可能让其他线程开启了二级缓存能够获取到缓存数据的,所以我们看下面sqlSession关闭提交的代码就明白了,要理解事务的概念,事务是干嘛的,就是为了产生脏数据而产生的一种控制,所以要明白这个概念,那么mybatis的二级缓存和一级缓存就明白了。

 

@Override
public void close(boolean forceRollback) {
  try {
    // issues #499, #524 and #573
    if (forceRollback) {
      tcm.rollback();
    } else {
      tcm.commit();
    }
  } finally {
    delegate.close(forceRollback);
  }
}

这个就是clsoe方法,这里肯定是执行到了commit,因为正常执行嘛,我们看下commit方法

 

public void commit() {
  for (TransactionalCache txCache : transactionalCaches.values()) {
    txCache.commit();
  }
}

transactionalCaches这个值又是从哪里来的呢?transactionalCaches就是在第一次查询出结果放入二级缓存的时候添加进去的,transactionalCaches这里面其实就是SynchronizedCache,所以这里循环这个集合,在看txCache的commit

 

public void commit() {
  if (clearOnCommit) {
    delegate.clear();
  }
  flushPendingEntries();
  reset();
}
private void flushPendingEntries() {
  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
    delegate.putObject(entry.getKey(), entry.getValue());
  }
  for (Object entry : entriesMissedInCache) {
    if (!entriesToAddOnCommit.containsKey(entry)) {
      delegate.putObject(entry, null);
    }
  }
}
private void reset() {
  clearOnCommit = false;
  entriesToAddOnCommit.clear();
  entriesMissedInCache.clear();
}

看到这里是不是就明白了,sqlSession1的关闭的会调用到了flushPendingEntries这个方法,这个方法将第一次放入二级缓存变量entriesToAddOnCommit中的数据拿出来重新放入了delegate的putObject中,是不是就明白了我上面说的缓存移动的一个概念,虽然不太准确,但是也是一种移动思想,先放入一个变量entriesToAddOnCommit,这个算是一个临时的变量吧,然后等第一个事务提交过后,将这个临时变量拿出来重新放入到delegate中,这个时候所有的线程只要是查询这个缓存说句都可以从delegate中得到缓存数据了,这个就是mybatis设计二级缓存的一个思想,整个思想中就是为了不能出现缓存脏数据而设计的一套机制,理解即可。

 

二级缓存的重要执行流程如上图,在网上有个博客有个图描述了整体的流程,我这里盗个图:

引用博客地址:https://blog.youkuaiyun.com/weixin_40516653/article/details/108412796

 

3.2.一级缓存

二级缓存分析完成过后,就开始分析一级缓存了,一级缓存就简单多了

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());
  //如果没有关闭session的情况下才可以执行查询
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    //如果在select中配置了flushCache=true,那么会清空一级缓存的数据
    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();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
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);
  //StatementType的默认是PREPARED
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

 

 

一级缓存非常简单,就是默认开启的,没有办法关闭的,这里 贴了源码和注释就不在做分析了,mybatis的一级缓存是session的缓存,所以是回话期间的缓存,如果要是session关闭或者提交的情况下,缓存就会关闭

InputStream input = Resources.getResourceAsStream("MyBatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
Object one = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println("第一次查询:"+one);
Object two = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);

System.out.println("第二次查询:"+two);

对象one和two的查询条件是一模一样的,那么第二次查询肯定是去缓存中查询,这里要清楚的一个概念是一级缓存针对的是内存缓存,而二级缓存是需要持久化到硬盘的缓存

因为one和two都是同一个session范围内的,所以two肯定是从缓存中获取的,看下输出:

sql只执行了一次,所以第二次查询肯定从缓存中读取的,而前面说了一级缓存是回话的缓存,那么如果我重新创建一个session呢?

 

Object one = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println("第一次查询:"+one);
Object two = sessionFactory.openSession().selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);

System.out.println("第二次查询:"+two);

输出:

所以一级缓存是针对session的缓存,因为缓存对象是在执行器中,而执行器是每次sesison开启的时候创建的,每次新创建的,所以一级缓存不能跨线程使用。

 

4.数据库查询核心分析(queryFromDatabase)

上面分析了缓存数据是如何管理的,一级缓存和二级缓存的关系,那么如果是第一次查询数据,不管是一级缓存还是二级缓存,都不会使用,这个时候就要去数据库查询了,所以这里简单分析下mybatis是如何去数据库查询的,mybatis查询数据库简单来说就是解析出了sql,然后创建ParameterHandler、StateMentHandler、ResultSetHandler等处理器去处理的,简单来说就是这些处理器完成了数据库的查询并且将查询结果封装成对象返回,其中ParameterHandler是参数处理器,就是处理参数的,StateMentHandler是处理JDBC预处理的,ResultSetHandler是处理查询结果的处理器,我们知道mybatis是ORM框架,什么是ORM框架,就是对象关系映射框架,所以最后一点就是要将查询的结果封装成O(Object)否则就不是ORM框架了,所以ResultSetHandler就是将查询结果封装成目标对象的,但是在这里需要明白的一点就是什么呢,在之前的笔记中写了mybatis中的插件机制,都知道了mybatis的4大插件,前面将了执行器Executor,执行器就是可以嵌入的一个插件,而其他三个插件就是ParameterHandler、StateMentHandler、ResultSetHandler,所以这里到时候就不演示插件的使用了。

从图片可以看出BaseExecutor中的queryFormDatabase方法其实它什么鸡毛也没干,BaseExecutor是一个组件抽象器,因为Executor是一个装饰器,所以这里交给了装饰器类去实现的这个数据库查询,所以要明白装饰器的一个设计理念。这里使用的执行器是默认的执行器也就是SimpleExecutor

 

4.1.SimpleExecutor.doQuery

 

 

@Override
  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();
      //这里通过装饰器模式得到一个StatementHander对象,根据StatementType得到一个StatementHandler,statementType默认类型为PREPARED,这个type是解析mapper默认设置的
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//    进行sql的预处理,获取一个连接,通过连接对象创建一个预处理对象,然后通过预处理对象填充参数
      stmt = prepareStatement(handler, ms.getStatementLog());
      //执行查询,然后封装查询结果
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

StatementHandler 这个的创建又是装饰器模式,不晓得mybatis的设计人员为什么这么喜欢这个模式,mybatis的源码中这个设计模式占比80%有的吧,我们先看下面怎么创建的

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  //根据策略模式得到一个StatementHandler
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  //在创建每个StatementHandler时,执行interceptorChain中所有的拦截器,也就是在执行每个sql的时候都会执行下拦截器中的plugin方法
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }
  ....

RoutingStatementHandler 是一个构件,其实它什么也不做,它把所有的事情都交给了具体的装饰器类delegate去执行的,我们看下类图:

 

所以了解这种模式就很清楚mybatis要干什么了。在创建PreparedStatementHandler的时候会去调用它的父类的构造BaseStatementHandler,在base中也进行了其他两个Handler的创建,以及插件的创建

 

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  this.configuration = mappedStatement.getConfiguration();
  this.executor = executor;
  this.mappedStatement = mappedStatement;
  this.rowBounds = rowBounds;

  this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  this.objectFactory = configuration.getObjectFactory();

  if (boundSql == null) { // issue #435, get the key before calculating the statement
    generateKeys(parameterObject);
    boundSql = mappedStatement.getBoundSql(parameterObject);
  }

  this.boundSql = boundSql;

  /**
   * 这里创建了参数处理器和结果集处理器
   * 并且会执行参数处理的插件和结果集处理的插件
   */
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

到这里mybatis的4大插件都已经分析完成了,下面就开始分析sql的预处理

 

4.1.1.prepareStatement

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  //获取一个连接对象Connection
  Connection connection = getConnection(statementLog);
  //获取一个预处理对象,通过Connection创建预处理对象(这个是调用jdbc本身的api)
  stmt = handler.prepare(connection, transaction.getTimeout());
  //参数填充(也是调用的jdbc的参数填充)
  handler.parameterize(stmt);
  return stmt;
}

 

上面的这个预处理没有啥可说的,跟下代码就 明白了,做的事情就是:

1.通过数据源获取一个连接;

2.通过连接进行sql预处理,得到预处理对象;

3.预处理对象进行参数填充;

4.最后返回这个预处理对象。

 

4.1.2.预处理查询数据库

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
 //执行查询
  ps.execute();
  //将查询出来的数据封装到对象中
  return resultSetHandler.handleResultSets(ps);
}

 

4.1.3.封装数据到对象

@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;
  /**
   * 包装第一条查询的数据对象,我们知道查询出来的对象都是ResultSet
   * 这里将查询出来的ResultSet.next对象封装到ResultSetWrapper中
   * 并且ResultSetWrapper中还包含了这一条数据锁包含的数据类型 ,实体属性,实体属性所对应的
   * class类型,比如
   * rs=ResultSet.next();
   * columnNames:实体属性列表如[id=null,name=null,age=null]
   * jdbcTypes:这一条数据的每一项数据的类型[INTEGER,VARCHAR,INTEGER]
   * classNames:实体属性所对应的class类型[java.lang.INTEGER,java.lang.String,java.lang.INTEGER]
   *所以下面要做的事情就是将rs中的数据根据columnNames、jdbcTypes、classNames得到一个实体对象,填充了
   * 实体对象的值,然后添加到multipleResults中,然后开始获取下一条 rsw = getNextResultSet(stmt);
   */
  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);
    //填充对象的属性值,然后添加到列表multipleResults中
    handleResultSet(rsw, resultMap, multipleResults, null);
    //获取下一条
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  /**
   *下面是对ResultSets的处理
   */

  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的整体执行流程就完成了,这里只分析了查询接口,更新删除插入流程也都差不多了,但是需要注意的是查询的时候如果开启了二级缓存或者开启了一级缓存,那么至少有一个缓存是开启的,所以在更新删除插入数据的时候就要清除缓存了,那么更新删除插入这里是怎么做的呢,这里简单看下,先看二级缓存的,因为只要开启了CacheEnable,那么执行器的装饰器构建CachingExecutor都会经过这里

 

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  flushCacheIfRequired(ms);
  return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache);
  }
}

在更新数据之前先要清空二级缓存的数据,如果开启了二级缓存并且flushCache设置为true的时候就会清空二级缓存,但是这个值默认是多少呢?如果细心的话,前面的解析全局配置里面就写了,flushCache这个值是只要非select的方法都是设置的true,所以说只要是更新、删除、插入都会调用这个clear方法来清空二级缓存的数据

而一级缓存呢?

 

一级缓存也会清空本地缓存,在更新、删除、插入的时候。

 

5.mybatis涉及的设计模式

Builder模式:例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;

工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;

单例模式:例如ErrorContext和LogFactory;

代理模式:Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;

组合模式:例如SqlNode和各个子类ChooseSqlNode等;

模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;

适配器模式:例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;

装饰者模式:例如Cache包中的cache.decorators子包中等各个装饰者的实现;

迭代器模式:例如迭代器模式PropertyTokenizer;

 

 

6.mybatis整合spring

mybatis如果是单独使用的话,会有些问题,比如说sqlSession,这个类就不是一个线程安全的类,而且如果单独使用的话一级缓存是没有办法关闭的,如果你不想在session的范围内缓存太多数据的话也是没有办法关闭的,但是现在基本上mybatis都没有单独使用的情况,有的也比较好了,现在基本上都是和spring进行整合的,至于myabtis如何交给spring管理的这个原理分析之前的spring专题就已经分析了,这里只说下为什么整合到spring中过后,那么就可以使一级缓存失效呢,这里稍后分析;还是大概看下是如何整合的吧,在官网上有很详细的说明,官网地址:http://mybatis.org/spring/zh/index.html ,在mybatis与spring整合的项目中有两个非常重要的类,一个是SqlSessionFactoryBean和SqlSessionTemplate,其实看了前面的分析都知道了,SqlSessionFactoryBean就是为了构建SqlSessionFacatory的,而SqlSessionTemplate就是为了构建SqlSession的,所以大概配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath*:mapping/*.xml" />
    </bean>


    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true" />
        <property name="username" value="root" />
        <property name="password" value="111111" />
        <property name="initialSize" value="1" />
        <property name="minIdle" value="1" />
        <property name="maxActive" value="10" />
        <property name="maxWait" value="60000" />
        <property name="timeBetweenEvictionRunsMillis" value="50000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20" />
        <property name="filters" value="stat" />
    </bean>


    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory" />

    </bean>


</beans>

 

 

SqlSessionFactoryBean:是一个工厂bean,在spring中是一个FactoryBean类型

public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
        ....

SqlSessionFactoryBean不仅仅是一个FactoryBean还是一个初始化bean还是一个监听器,做的事儿有点多哦

 

既然是一个FactoryBean,那么在得到实例对象的其实是调用的getObject

 

@Override
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}

返回的就是一个SqlSessionFactory,而afterPropertiesSet是在初始化后会来调用的,这里getObejct的时候如果为空,也会来调用

afterPropertiesSet:

 

@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
      "Property 'configuration' and 'configLocation' can not specified with together");

  this.sqlSessionFactory = buildSqlSessionFactory();
}
//和mybatis的读取全局配置文件一样的流程
//只是这里不是读取的配置文件,而是整合了spring过后,通过参数设置注入得到
//包括mapper配置文件的读取解析,都是一样的,只是换了一种方式而已
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

  final Configuration targetConfiguration;

  XMLConfigBuilder xmlConfigBuilder = null;
  if (this.configuration != null) {
    targetConfiguration = this.configuration;
    if (targetConfiguration.getVariables() == null) {
      targetConfiguration.setVariables(this.configurationProperties);
    } else if (this.configurationProperties != null) {
      targetConfiguration.getVariables().putAll(this.configurationProperties);
    }
  } else if (this.configLocation != null) {
    xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
    targetConfiguration = xmlConfigBuilder.getConfiguration();
  } else {
    LOGGER.debug(
        () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
    targetConfiguration = new Configuration();
    Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
  }

  Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
  Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
  Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

  if (hasLength(this.typeAliasesPackage)) {
    scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
        .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
        .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
  }

  if (!isEmpty(this.typeAliases)) {
    Stream.of(this.typeAliases).forEach(typeAlias -> {
      targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
      LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
    });
  }

  if (!isEmpty(this.plugins)) {
    Stream.of(this.plugins).forEach(plugin -> {
      targetConfiguration.addInterceptor(plugin);
      LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    });
  }

  if (hasLength(this.typeHandlersPackage)) {
    scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
        .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
        .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
  }

  if (!isEmpty(this.typeHandlers)) {
    Stream.of(this.typeHandlers).forEach(typeHandler -> {
      targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
      LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
    });
  }

  targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);

  if (!isEmpty(this.scriptingLanguageDrivers)) {
    Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
      targetConfiguration.getLanguageRegistry().register(languageDriver);
      LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
    });
  }
  Optional.ofNullable(this.defaultScriptingLanguageDriver)
      .ifPresent(targetConfiguration::setDefaultScriptingLanguage);

  if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
    try {
      targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
    } catch (SQLException e) {
      throw new NestedIOException("Failed getting a databaseId", e);
    }
  }

  Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

  if (xmlConfigBuilder != null) {
    try {
      xmlConfigBuilder.parse();
      LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
    } catch (Exception ex) {
      throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  targetConfiguration.setEnvironment(new Environment(this.environment,
      this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
      this.dataSource));

  if (this.mapperLocations != null) {
    if (this.mapperLocations.length == 0) {
      LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
    } else {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
        LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
      }
    }
  } else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
  }

  return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

这个方法最核心的方法就是buildSqlSessionFactory,构建一个SqlSessionFactory,这个方法做的事情就是和mybatis启动读取全局配置文件的流程一样的,最终将全局配置文件读取完成过后构建出一个DefaultSqlSessionFactory返回。

 

6.1.SqlSessionTemplate

 

前面说了mybatis中的SqlSession不是一个线程安全的类,为什么呢?因为在一个线程中,如果只有一个SqlSession,那么它就不是一个线程安全的,在mybatis中,SqlSession只有一个实现也就是DefaultSqlSession

官方都写了注释表示它不是一个线程安全的类,所以在多线程下是有线程安全问题的,那么整合了spring过后就成了一个线程安全的类了,那么spring是怎么做到的呢?所以这些答案也就在SqlSessionTemplte中,这里重点分析下怎么就变成了一个线程安全的类了。

在spring的配置文件中注入的时候是这样写的:

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory" />

    </bean>

这是通过构造方法进行注入的,并且注入的是一个sqlSessionFactory,所以在SqlSessionTemplate要找这个构造方法:

 

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
}
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

最终会调用到这个构造方法,看到上面的最后一行代码了吗,sqlSessionProxy是一个SqlSession,也就是操作CRUD的一个类,这里创建的是一个代理类,具体的代理是SqlSessionInterceptor,所以mybatis整合了spring的核心是一个线程安全的类应该就是这个代理类起的作用

SqlSessionInterceptor

/**
 * Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
 * unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to the
 * {@code PersistenceExceptionTranslator}.
 * 这个是创建sqlSession的时候的一个代理类,这个代理是JDK的动态代理Proxy
 * 看到这个代理类,肯定就直接看invoke方法了
 *
 */
private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //先去得到一个SqlSession
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      /**
       * 为什么说整合了spring过后,mybatis的一级缓存就失效了,看到下面的代码sqlSession.commit(true)
       * 是不是就明白了,分析mybatis源码的时候过后,mybatis的一级缓存是session的缓存,只有等
       * session提交或者关闭的情况下,一级缓存才会失效,所以整合了spring过后,每次使用完成了自己提交了
       * 所以一级缓存就失效了。
       */
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
      ......
      }
      /**
 *
 * @see SpringManagedTransactionFactory
 * 这个方法的核心在于TransactionSynchronizationManager,这个是spring的事务管理器,还记得spring专题的时候说过
 * spring的事务管理器就是将所有的操作放入线程的缓存中来控制事务的,那么其实这里也是类似的
 * 比如说在多线程中如何保护线程安全,就是将每个线程创建的对象放入线程缓存中ThreadLocal,然后每个线程操作自己的变量对象
 * 这样就会达到线程安全的目的
 * 所以这里的执行逻辑是每次线从当前线程缓存中获取整个SqlSession,如果有,证明这个SqlSession之前当前线程的之前操作中已经放入了
 * 如果没有,则证明我这个线程是第一个来拿这个SqlSession,然后创建了,放进去
 * 所以核心就在于ThreadLocal来保证的Sqlsession的线程安全
 */
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

  //从事物管理器中拿到SqlSessionHolder,看当前线程是否已经创建了
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
 //创建SqlSesssion
  SqlSession session = sessionHolder(executorType, holder);
  //如果创建了,就直接返回
  if (session != null) {
    return session;
  }

  //没有这里就开始创建一个新的SqlSession,创建流程就是调用了mybatis的openSession

  LOGGER.debug(() -> "Creating a new SqlSession");
  session = sessionFactory.openSession(executorType);
  //然后将新创建的注册进去,以便当前线程下次还需要
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

 

 

 

 

 

深入解析Spring与MyBatis的整合及源码实现 在开始探索Spring框架的核心之前,我们必须先了解它是什么以及它为何对现代Java开发至关重要。Spring是一个开源的、全面的企业级应用开发框架,提供了一种简化Java应用程序开发的方式。它不仅帮助开发者编写可维护的代码,还能通过依赖注入(DI)和面向切面编程(AOP)来管理业务对象之间的复杂关系。MyBatis 还具有一些高级特性,例如:动态 SQL: MyBatis 支持动态 SQL 标签,如<if><choose><foreach> 阅读详情

相关推荐

华为星闪SLE实战:手把手教你用HarmonyOS开发智能家居控制应用(附完整代码)

本文详细介绍了如何利用华为星闪SLE技术,结合HarmonyOS开发智能家居控制应用。通过对比传统无线技术,阐述了星闪SLE在低时延、高可靠性和高并发方面的优势,并提供了从环境搭建、设备发现、连接管理到核心控制代码的完整实战教程,帮助开发者快速上手这一无线短距通信技术。

bert9linguist的博客 503

mybatis源码分析系列(开胃菜)-mybatis中的执行器架构体系

mybatis中sqlSession与各种执行器关系介绍。其中包括Executor、CachingExecutor、BaseExecutor、SimpleExecutor、ReuseExecutor、BatchExecutor等

weixin_44637227的博客 454

深入剖析STM32F103 I2C总线死锁问题及高效应对策略

本文深入剖析了STM32F103硬件I2C总线在轮询模式下发生死锁的根本原因,即硬件状态机的预取机制与软件响应速度不匹配。文章提供了高效的解决方案,包括在轮询模式下提前发送NACK/STOP信号、降低总线速度,以及治本的中断与DMA应用策略,帮助开发者根据项目需求选择最合适的方案,彻底解决这一经典硬件缺陷问题。

weixin_29184371的博客 204

Mybatis源码(3)-查询执行流程

上文介绍了mapper的创建过程,mybatis通过动态代理的方式生成了代理,MapperProxy实现了InvocationHandler类。下文以查询流程为例,分析sql的执行过程。 MapperProxy: @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.g

闪电大王 1376

主流双目相机技术参数横向评测:从ZED到INDEMIND的深度解析

本文对主流双目相机进行了深度横向评测,涵盖ZED、Bumblebee、Leap Motion、DUO 3D及INDEMIND等国内外产品。文章详细解析了分辨率、帧率、视场角、基线等核心参数,并结合科研、机器人导航、无人机、工业检测等典型应用场景,提供了实战选型指南,帮助读者根据具体需求挑选最合适的双目视觉解决方案。

weixin_29212867的博客 639

DETR系列文章之–Co-DETR

Co-DETR,并行训练多个辅助头,以一对多的标签分配方式进行监督从辅助头中提取正样本坐标,为解码器中的正样本的训练效率进行额外的定制化正样本query.一对一标签分配:每个GT框分配给一个特定的query(DETR),较少的正query导致低效训练,从编码器生成的潜在表示和解码器中的注意力学习分析这一问题。一对多标签分配:每个GT框分配给检测器输出的多个proposal/anchor,依赖NMS或者anchor生成(ATSS,RetinaNet,FCOS,PAA)。

weixin_43731232的博客 2096

mybatis学习(一)之加载全局配置文件以及映射文件

前言 本篇博客适合新手看,对于工作一两年的我们,我们要思考为什么学习mybatis源码,我的回答是,一是为了面试,面试造火箭。二是为了排查工作上遇到的一些问题,三是mybatis为我们做了什么,它的基本原理是什么,我从中可以学习到什么,举一反三。 最后说一句,不要把太多时间花在框架上,框架说白了就是帮你做了一些重复的事情,并且解决了一些额外的问题,而我们要学习的就是解决问题的思想。当然,Spring多花时间学没关系,这个框架很稳,未来几年应该不会被淘汰,哈哈。 1.mybatis加载全局配置文件.

Riztal的博客 510

实现两端固支梁热力耦合的有限元分析

在本文中,我们将使用Matlab实现两端固支梁的热力耦合有限元分析。我们假设梁的材料是均匀的,梁的截面积为1,长度为1,两端固支,且受到均匀分布的热荷载。接下来,我们需要组装全局刚度矩阵和热传导矩阵。对于两个相邻的元素,它们共享一个节点,因此需要使用连接矩阵将局部矩阵组装成全局矩阵。接下来,我们需要定义元素刚度矩阵和热传导矩阵。然后,我们需要定义载荷向量。然后,我们需要定义边界条件。在这个例子中,梁的两端固支,因此我们需要将两端节点的位移限制为0。最后,我们需要解决线性方程组,得到节点的位移和温度分布。

CyberSparkZ的博客 394

Mybatis深入源码分析之SQLSession一级缓存原理分析

通过前面几篇文章,Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析,Mybatis深入源码分析之Mapper与接口绑定原理源码分析。我们对Mybatis源码也有了一定的了解。本篇文章,我们继续分析:SQLSession一级缓存原理。 一:invoke...

chuanyingcao2675的博客 261

Unity Sprite Atlas C++底层原理全解析 —— “像素聚合工厂”的秘密

Unity Sprite Atlas C++底层实现简析 Sprite Atlas是Unity中用于优化2D渲染性能的核心机制,其C++底层实现如同一个高效的"像素聚合工厂"。该系统通过将多个小纹理合并为一个大纹理,显著减少Draw Call次数和GPU纹理切换开销。 在数据结构上,底层采用SpriteAtlasData存储图集元数据,PackedTexture管理合并后的大纹理,SpriteFrame记录每个子精灵的位置信息,并通过哈希表实现快速查找。编辑器阶段使用最大矩形等算法优化空

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 203

2023年下半年网络工程师案例分析真题答案解析

R1] ip route-static 0.0.0.0 0.0.0.0 GigabitEthemet2/0/1 (3) track (4) R1 to R2 //配置R1的缺省路由,联动BFD使得R1到2的流量优先走R1->S1->R2链路,当此链路发生故障时,流量切换到R1->R3->R2链路上。当同一VLAN内也需要网络隔离时,我们可采用交换机的(2)功能,若在配置了二层隔离后,部分主机有互通的需求,则可在需求主机的网关上配置(3)功能。不需要,MAC地址是数据链路层封装以太帧的地址标识。

鹿鸣天涯 744

Mybatis 源码分析(二)之 Mybatis 操作数据库的流程

Mybatis 源码分析(二)之 Mybatis 操作数据库的流程 承接之前的 Mybatis 源码分析(一)之 Mybatis的Executor的初始化 继续进行之后的源码分析。 在之前的分析中,我们知道初始化Executor的流程。 SqlSessionFactoryBuilder-&amp;gt;parse-&amp;gt;Configuation-&amp;gt;build-&amp;gt;SqlSessionFac...

挂机的阿洋zzZ 471

OpenClaw进阶实战(二十五):抖音企业号——私信回复与视频评论分析

摘要:本文介绍抖音企业号自动化运营方案,突破抖音强风控体系的技术挑战。针对内容生产、私信互动和账号矩阵三大场景,提出混合架构解决方案: 私信自动化:结合关键词触发与随机延迟(45秒±15),实现拟人化回复,支持价格咨询/商务合作等高优先级消息识别 混合技术栈:官方API(合规)+真机RPA(覆盖长尾需求)+AI辅助生成(脚本→剪辑→发布闭环) 安全策略:通过设备指纹模拟、行为间隔随机化和内容差异化处理规避风控,提供每日200条的安全操作阈值。配套OpenClaw技能实现一键部署,包含cookie配置、回复规

PeterPan的博客 139

mybatis整合spring后一级缓存真的会失效吗?

mybatis整合spring后一级缓存真的会失效吗?前言1.代码案例2.源码分析总结 前言 在网上看了很多的文章都表示mybaits和spring整合后一级缓存会“失效”,或者说是达到了失效的效果,也就是多次查询不会命中一级缓存,大致原因如下: 那么真的如上图说的一样吗,每一次查询都会开启一个sqlsession,所以导致了一级缓存“失效”的效果吗?我们一起来探究下 1.代码案例 首先如果我们...

weixin_40969748的博客 3139

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Spring整合Mybatis源码剖析

其核心在于将其他框架所生成的类注入至Spring容器中,实现框架间的无缝对接。以Spring整合Mybatis为例,该过程主要是将Mybatis所生成的Mapper接口代理对象作为Bean注册至Spring容器中,从而便于在Spring环境中对Mapper接口进行依赖注入和事务管理。

x1ao_fe1的博客 960

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【Mybatis+spring整合源码探秘】--- Mybatis整合Spring后在非事务情况下一级缓存失效的底层原理

文章目录1 现象2 必备前置知识3 源码探寻入口 源码地址:https://github.com/nieandsun/mybatis-study 1 现象 如下图所示: 连续两次调用userMapper的同一个查询方法,第二次却没有像Mybatis单独开发时一样走缓存 —》本文将从源码的角度来探索一下其具体的原因。 2 必备前置知识 通过文章《【Mybatis+spring整合源码探秘】...

nrsc 2744

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

mybatis+spring执行流程以及一级缓存为何失效

springboot+mybatis一级缓存遇到的问题

weixin_44289936的博客 950

spring boot mybatis 整合_Spring整合Mybatis一级缓存失效?

Spring整合Mybatis一级缓存失效?❝一般来说,可以在5个方面进行缓存的设计:最底层可以配置的是mysql自带的query cachemybatis的一级缓存,默认情况下都处于开启状态,只能使用自带的PerpetualCache,无法配置第三方缓存mybatis的二级缓存,可以配置开关状态,默认使用自带的PerpetualCache,但功能比较弱,能够配置第三方缓存service...

weixin_39850699的博客 321

转载:spring 整合mybatis后用不上session缓存的原因分析

因为一直用spring整合了mybatis,所以很少用到mybatis的session缓存。 习惯是本地缓存自己用map写或者引入第三方的本地缓存框架ehcache,Guava 所以提出来纠结下 实验下(spring整合mybatis略,网上一堆),先看看mybatis级别的session的缓存 放出打印sql语句 configuration.xml 加入

冬冬的博客 844

mybatis-spring中的缓存失效问题

一. mybatis中的一级缓存 首先我们来说一下mybatis中的一级缓存。 mybatis中使用默认的SQLSession——DefaultSqlSession,每一个DefaultSqlSession中都持有一个Executor——BaseExecutor,BaseExecutor中含有一个属性localCache,localCache便是mybatis中的一级缓存。当用户初次发起查询时,会从数据库查询数据,并将查询结果保存在localCache中。 private <E> List&l

weixin_45359574的博客 453

mybatis执行流程

mybatis执行流程前提:正文我们从一个简单的查询开始开始执行mapperMethod.executeexecuteForManyDefaultSqlSessionexecutor 的执行到此一次查询就结束了总结 前提: 本次讲解的环境是在spring与mybatis整合的情况下进行的源码分析,整合的部分就不细说了。 正文 我们从一个简单的查询开始 可以看到,我们自己定义的接口其实最终成为了 ...

Lee_Suoer的博客 701

深入浅出Mybatis

本专栏系统讲解MyBatis框架核心技术,包含基础使用、高级查询、Spring整合、缓存机制和性能优化五大模块。针对不同学习者提供个性化学习路径:新手可从环境搭建循序渐进,进阶者重点攻克动态SQL和缓存原理,面试者可专注源码分析和设计模式。每章节均配有代码示例和常见问题解答,通过"理论+实践"方式帮助开发者从入门到精通。特别强调动手能力,建议本地复现代码以加深理解。

fykam的博客 3144

mybatis的一级缓存为什么在spring中会失效

一、问题描述 mybatis的一级缓存是基于session来的,当单独使用mybatis的时候,一级缓存是起作用的,在一个session中查询2遍同样的sql,只会打印一次sql语句。但当mybatis与spring搭配使用后,mybatis的一级缓存就会失效,会打印2次sql。 @Service public class TestService { @Autowired Tes...

qiuz1024的博客 1784

Mybatis源码执行流程分析(包含整合spring及springboot的整合原理)

我们都知道,mybatis框架是一个半自动的ORM框架,可以简化我们对于数据库连接的管理,以及可以对于所有的查询配置的统一维护。是对于传统的JDBC连接操作的二次封装。这样其实我们都需要手动的获取连接数据,关闭流数据。为了解决上面的一些列问题,mybatis引入了一系列的组件来解决出现的问题。

m0_37903574的博客 446

mybatis源码分析

spring和mybatis源码分析 mybatis的流程分析 在整合spring的情况下,mybatis从哪里开始 首先分析得知mybatis和spring整合的情况下,主要是提供两处关联 @MapperScan @Bean---->SqlSessionFactoryBean 通过源码得知MapperScan其实主要利用spring的Import技术和spring的扩展点ImportBe...

qq_24101357的博客 2万+

MyBatis学习笔记

教学视频 - - - 1.简介 2.下载 3.HelloWorld 4.接口式编程 5.小结(1) 6.全局配置文件-引入dtd约束 7.全局配置文件-properties-引入外部配置文件 8.全局配置文件-settings-运行时行为设置 9.全局配置文件-typeAliases-别名 10.全局配置文件-typeHandlers-类型处理器简介 11.全局配置文件-plugins-插件简介 12.全局配置文件-enviroments-运行环境 13.全局配置文件-dat

KISS 9818
上一篇: 2.[mybatis]全局配置执行流程源码分析
下一篇: 1.[springMvc]Servlet的基础知识
Life  journey
博客等级 码龄17年 1万+粉丝 80原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值