Mybatis关键源码深度分析学习-插件与缓存

一、mybatis的插件源码解析

为了保证程序的扩展性,提供使用者在框架基础上增强自定义的逻辑处理,mybatis提供了一种基于拦截器的插件机制。

Mybatis插件相关的接口或类有:Intercept、InterceptChain、Plugin和Invocation,这几个接口或类实现了整个Mybatis插件流程。

我们先从Debug流程分析开始:

准备步骤:

1、自定义实体类:

@Intercepts
        ({
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
        })
public class SqlRunningTimeInterceptor implements Interceptor{

    private static final Logger log = LoggerFactory.getLogger(SqlRunningTimeInterceptor.class);


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        String statementId = mappedStatement.getId();
        long end = System.currentTimeMillis();
        long timing = end - start;
        if(log.isInfoEnabled()){
            log.info("执行sql耗时:" + timing + " ms" + " - id:" + statementId + " - Sql:" );
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

这里自定义拦截器类实现Interceptor接口。在Mybatis中想实现插件就必须实现Interceptor接口。

 

两个注解:

Intercepts注解用于Interceptor的实现类,用于注明拦截后需要处理的类和方法。

Signature表示对Executor的增删改查方法拦截后通过intercept方法覆盖原逻辑

看看sql的执行消耗时间,结果如下:

在讲mybatis的执行原理有提到过mybatis的插件,在构建sqlSession的工厂时,会解析mybatis-config.xml的配置信息,如果自定义了插件类,就回去解析。断点如下:

进入:pluginElement方法

我们看看他实例化的类是哪个类

他是实例化我们的自定义的拦截插件类。

进入org.apache.ibatis.session.Configuration#addInterceptor方法

这个InterceptorChain类其实就是由Interceptor组成的列表类,pluginAll方法就是循环遍历Interceptor#plugin(Object)方法,将被拦截的对象层层进行代理,得到一个最终的代理对象。(责任链模式)

 

Mybatis支持Executor, StatementHandler, ParameterHandler, ResultSetHandler进行拦截,这是因为在Configuration类中生成对应对象时调用了InterceptorChain#pluginAll(Object)方法,返回的是代理对象。

创建会话,构建执行器时,org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)方法会执行org.apache.ibatis.plugin.InterceptorChain#pluginAll方法

进入org.apache.ibatis.plugin.Plugin#wrap方法(plugin方法其实调用的是Plugin的wrap函数,wrap函数是用来生代理类的目标类的):

Plugin继承了InvocationHandler接口,使用了jdk动态代理。

可以看到getSignatureMap方法其实就是获取到自定义的类和其中的方法。

 

执行sql语句:

前面执行流程相同不再叙述,进入org.apache.ibatis.plugin.Plugin#invoke

 

回到自定义拦截类

执行org.apache.ibatis.plugin.Invocation#proceed方法

通过invoke会去执行org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

发现mybatis执行流程会在自定义拦截器类主导

执行完sql语句后会再回到自定义插件类中,如此便可以计算出sql语句的执行时间

当然拦截器还有其他的用处,字段类型转换,分页,监控,数据埋点等等

总结:Mybatis的插件的原理就是对Executor、ParameterHandler、ResultSetHandler和StatementHandler的实现类通过Plugin代理来生成目标类,并且关联生成的目标类与实现的Interceptor类,这样在调用Executor、ParameterHandler、ResultSetHandler和StatementHandler的实现类方法时会调用Plugin类中的invoke方法,在调用invoke方法时会进行判断这个实现类是否需要我们实现的MyInterceptor来处理,如果是会调用MyInterceptor的interceptor方法,进行一些处理之后调用Invocation的proceed方法,其实际处理方法也是调用method.invoke方法,如果不需要MyInterceptor处理时则直接调用method.invoke方法,Mybatis巧妙的利用的JDK的代理机制,实现了对目标类和方法在调用之前的一些处理,提高了整个框架的灵活性。

=====================分割线==========================

 

二、mybatis的缓存源码解析

MyBatis将数据缓存设计成两级结构,分为一级缓存、二级缓存。

一级缓存是Session会话级别的缓存,位于表示一次会话的SqlSession对象之中,又被称之为本地缓存。一级缓存是MyBatis内部实现的一个特性,用户不能配置,默认情况下一级缓存自动开启。

每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。

为什么要使用一级缓存呢?

     1、 在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。

   2、  为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

     3、MyBatis会在一次会话的表示----一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。

下面我们通过源码去看看一级缓存有什么特征

对数据库同一笔数据查询两次。结果如下:


 我们看到sql语句真正去执行的只有一次,也就是去数据查询次数一次,另一个其实就是从一级缓存去拿的。

断点看源码:

 

进入:org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)方法

进入:createCacheKey方法创建缓存

 

创建缓存后返回(会将cachekey作为Map的key)

进入查询我们看看第一次查询

进入:org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)方法。

这里有一个判断逻辑如果在缓存中查询到这个数据,那就从缓存走,否则从数据库查询

第一次查询list肯定为空,然后去数据库中查询。然后我们进入org.apache.ibatis.executor.BaseExecutor#queryFromDatabase方法

这个list就是查询结果,然后进入创建本地缓存值

我们看到这个key其实就是创建缓存的实例对象。

我们看这个这时候已经到数据库查询了一次。下面再去执行一次。看看他的流程是从数据库还是缓存。

进入:createCacheKey方法,去创建缓存(生成map的key)

进入:org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)方法

我们看到从缓存中拿到数据,那么我就会从直接从缓存拿去数据减少一次查询数据库的次数。

结果如下:

两次查询只执行了一次sql语句。

总结:流程如图

由于MyBatis使用SqlSession对象表示一次数据库的会话,那么,对于会话级别的一级缓存也应该是在SqlSession中控制的。

实际上, MyBatis只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。SqlSession、Executor、Cache之间的关系如下列类图所示:

如上述的类图所示,Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,则对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存。

综上,SqlSession对象、Executor对象、Cache对象之间的关系如下图所示:

由于Session级别的一级缓存实际上就是使用PerpetualCache(就是localCache存放从数据库查询到的数据)维护的。

PerpetualCache实现原理其实很简单,其内部就是通过一个简单的HashMap<k,v> 来实现的,没有其他的任何限制。

一级缓存的生命周期有多长?

a. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

b. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;

c. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;

d.SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;

最后注意一个问题:

MyBatis认为的完全相同的查询,不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了。

 

Mybatis的二级缓存:

MyBatis缓存设计及二级缓存工作模式

如上图所示,当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。

CachingExecutorExecutor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式。

MyBatis二级缓存的划分:

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:

a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);

b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);

如果你想让多个Mapper公用一个Cache的话,你可以使用<cache-ref namespace="">节点,来指定你的这个Mapper使用到了哪一个Mapper的Cache缓存。

 使用二级缓存,必须要具备的条件:

MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。

     虽然在Mapper中配置了<cache>,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,其中 select 默认 userCache=true。

总之,要想使某条Select查询支持二级缓存,你需要保证:

 1.  MyBatis支持二级缓存的总开关:全局配置变量参数   cacheEnabled=true

  2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效

一级缓存和二级缓存的使用顺序
      请注意,如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:

               二级缓存    ———> 一级缓存——> 数据库

 

二级缓存实现的选择:


    MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache  type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached的集成,总之,使用MyBatis的二级缓存有三个选择:

        1.MyBatis自身提供的缓存实现;

        2. 用户自定义的Cache接口实现;

        3.跟第三方内存缓存库的集成;

 

MyBatis自身提供的缓存实现:

有一级缓存知识可以知道,这个会执行两次sql语句去数据库查询。如果我们开启了二级缓存,结果如下:

只需要一次就可以了。下面代码是关键的地方(一级与二级不同的地方)

 @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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将结果放入二级缓存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

 

跟第三方内存缓存库的集成

mybatis自身方法,当你关闭服务,则二级缓存会消除。这里介绍下redis第三方库的方法。

第一次执行:只有一条sql语句

第二次执行:0条 直接从redis里面拿取

自定义redis缓存类:

@Slf4j
public class MybatisRedisCache implements Cache {
  private Jedis redisClient = createReids();
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  private String id;

  public MybatisRedisCache(final String id) {
    if (id == null) {
      throw new IllegalArgumentException("Cache instances require an ID");
    }
    log.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
    this.id = id;
  }

  @Override
  public String getId() {
    return this.id;
  }

  @Override
  public int getSize() {
    return Integer.valueOf(redisClient.dbSize().toString());
  }

  @Override
  public void putObject(Object key, Object value) {
    log.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value);
    redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
  }

  @Override
  public Object getObject(Object key) {
    Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
    log.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value);
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
  }

  @Override
  public void clear() {
    redisClient.flushDB();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return readWriteLock;
  }

  protected static Jedis createReids() {
    Jedis jedis=new Jedis("192.168.19.102",6379);
    jedis.auth("test123");
    return jedis;
  }
}

 

结束===========继续努力。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值