第四章 mybatis源码系列-mybatis的高级应用

本文深入探讨MyBatis的一级缓存与二级缓存机制、Mapper代理方式及插件原理,帮助读者理解其内部运作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、缓存

 1.1 缓存-一级缓存的设计与原理

1.2 缓存-二级缓存的设计与原理

1.2.1 开启二级缓存

1.2.2 二级缓存源码分析

二、Mapper代理方式

2.1 getmapper()

2.2 invoke()

三、mybatis插件

3.1 插件介绍

3.2 插件原理

3.3 简单应用


一、缓存

 1.1 缓存-一级缓存的设计与原理

   一级缓存基于 SqlSession ,所以我们可以直接创建 SqlSessionFactory ,并从中开启一个新的 SqlSession ,默认情况下它会自动开启事务,所以一级缓存会自动使用。

实际我们在第三章讲解excute的query源码里面就有一级查询的源码,我们来看一下:

Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {
     BoundSql boundSql = ms.getBoundSql(parameter);
    //创建缓存key
     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ......
    // 如果statement指定了需要刷新缓存,则清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 查询之前先检查一级缓存中是否存在数据
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 有,则直接取缓存
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 没有,则查询数据库
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        // ......
        // 全局localCacheScope设置为statement,则清空一级缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

这里查询的时候直接先去根据查询statment的特性拼接CacheKey,然后根据CacheKey查一级缓存,有的话直接存入拿出赋值给查询结果。

这个缓存的生成是什么时候呢:

// queryFromDatabase 方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql
boundSql) throws SQLException {
   List<E> list;
   localCache.putObject(key, EXECUTION_PLACEHOLDER);
   try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
   } finally {
      localCache.removeObject(key);
}
   localCache.putObject(key, list);
   if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
}
   return list;
}

以上代码说明:一级缓存放置时机,实际就是queryFromDatabase方法中,直接将查询数据库结果封装后结果类放到一级缓存。

    总结一下可以发现,一级缓存起作用的位置,是在向数据库发起查询之前,先拦截检查一下,如果一级缓存中有数据,则直接从缓存中取数据并返回,否则才查询数据库,然后放入一级缓存。 

    实际一级缓存是有一定问题的,大家看源码会发现,一级缓存操作直接操作的都是一个对象,将查询结果直接放入缓存,然后查询缓存赋值给结果的是相同对象地址,导致在同一个sqlsession中查询后,对结果类进行修改,在进行查询,会查询缓存拿到修改后的对象,这是需要注意的。

1.2 缓存-二级缓存的设计与原理

    二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件namespace的,也 就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。

1.2.1 开启二级缓存

和一级缓存默认开启不一样,二级缓存需要我们手动开启。最最简单的使用方式,只需要在 mapper.xml 上打一个 <cache /> 标签,就算开启二级缓存了:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache />
    <!-- ...... -->
</mapper>

对应的 Mapper 接口,则需要用 @CacheNamespace 注解开启:

@CacheNamespace
public interface DepartmentMapper {
    // ......
}

另外,不要忘记给实体类实现 Serializable 接口,否则二级缓存也是不能用的。

1.2.2 二级缓存源码分析

CachingExecutor

// CachingExecutor
public <E> List<E> 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);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds
rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  // 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
  // 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
  // 我们在上面解析blog.xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
  Cache cache = ms.getCache();
  // 如果配置文件中没有配置 <cache>,则 cache 为空
  if (cache != null) {
    //如果需要刷新缓存的话就刷新:flushCache="true"
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      // 访问二级缓存
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 缓存未命中
      if (list == null) {
        // 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的
话,则进行DB查询
        list = delegate.<E>query(ms, parameterObject, rowBounds,
resultHandler, key, boundSql);
        // 缓存查询结果
        tcm.putObject(cache, key, list);
     }
      return list;
   }
 }
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key,boundSql);
}

在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。

  • 二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
  • 二级缓存具有丰富的缓存策略。
  • 二级缓存可由多个装饰器,与基础缓存组合而成
  • 二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache完成。

    一级缓存在第一次查出数据后,在一个sqlsession中,若果直接修改该数据,之后第二次查询时,从一级缓存中查出来的数据是被修改过的,并非数据库的真实数据,原因是 MyBatis 利用一级缓存是直接将数据的引用交出去了。

    二级缓存就不一样了,我们从二级缓存中查出来的数据那可是跨 SqlSession 的,谁知道你改不改数据(还不敢保证改的对不对),万一你改了那别人从二级缓存中拿的数据就是被你改过的,这样万一出点问题,那可就出大事了。MyBatis 自然帮我们考虑到了这一点,于是它给二级缓存设计了一个只读属性。这个只读属性如果设置为 true ,则通过二级缓存查询的数据会执行一次基于 jdk 序列化的对象深拷贝,这样就可以保证拿到的数据不会对原二级缓存产生影响(但一次对象的深拷贝会导致性能降低);而 readOnly 设置为 false ,则只读的缓存会像一级缓存那样,直接返回二级缓存本身,虽然可能不安全,但好在处理速度快

二、Mapper代理方式

2.1 getmapper()

进入源码 sqlSession.getMapper(UserMapper.class )中:

//DefaultSqlSession 中的 getMapper
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
 }

//configuration 中的给 getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
 }
  //MapperRegistry 中的 getMapper
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //从 MapperRegistry 中的 HashMap 中拿 MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>)
knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the
MapperRegistry.");
   }
    try {
      //通过动态代理工厂生成示例。
      return mapperProxyFactory.newInstance(sqlSession);
   } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: "
+ e, e);
   }
 }

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

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

2.2 invoke()

    在动态代理返回了示例后,我们就可以直接调用mapper类中的方法了,但代理对象调用方法,执行是在MapperProxy中的invoke方法中:

public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
    try {
      //如果是Object定义的方法,直接调用
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
     } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
     }
   } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
   }
    // 获得 MapperMethod 对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //重点在这:MapperMethod最终调用了执行的方法
    return mapperMethod.execute(sqlSession, args);
 }

进入execute方法:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //判断mapper中的方法类型,最终调用的还是SqlSession中的方法 switch
(command.getType()) {
    case INSERT: {
      //转换参数
      Object param = method.convertArgsToSqlCommandParam(args);
      //执行INSERT操作
      // 转换 rowCount
      result = rowCountResult(sqlSession.insert(command.getName(),
param));
      break;
   }
    case UPDATE: {
      //转换参数
      Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
      result = rowCountResult(sqlSession.update(command.getName(),
param));
      break;
   }
    case DELETE: {
      //转换参数
      Object param = method.convertArgsToSqlCommandParam(args);
      // 转换 rowCount
      result = rowCountResult(sqlSession.delete(command.getName(),
          param));
      break;
   }
    case SELECT:
    //无返回,并且有ResultHandler方法参数,则将查询的结果,提交给 ResultHandler 进行
处理
    if (method.returnsVoid() && method.hasResultHandler()) {
      executeWithResultHandler(sqlSession, args);
      result = null;
    //执行查询,返回列表
   } else if (method.returnsMany()) {
      result = executeForMany(sqlSession, args);
    //执行查询,返回Map
   } else if (method.returnsMap()) {
      result = executeForMap(sqlSession, args);
    //执行查询,返回Cursor
   } else if (method.returnsCursor()) {
      result = executeForCursor(sqlSession, args);
    //执行查询,返回单个对象
   } else {
    //转换参数
      Object param = method.convertArgsToSqlCommandParam(args);
    //查询单条
      result = sqlSession.selectOne(command.getName(), param);
      if (method.returnsOptional() &&
         (result == null ||
              !method.getReturnType().equals(result.getClass())))
{
        result = Optional.ofNullable(result);
     }
   }
    break;
    case FLUSH:
    result = sqlSession.flushStatements();
    break;
    default:
    throw new BindingException("Unknown execution method for: " +
command.getName());
 }
    //返回结果为null,并且返回类型为基本类型,则抛出BindingException异常
    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;
}

三、mybatis插件

3.1 插件介绍

    一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。

3.2 插件原理

    Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件
(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。

MyBatis所允许拦截的方法如下:

  • 执行器Executor (update、query、commit、rollback等方法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters方法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法); 

在四大对象创建的时候

  • 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
  • 获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返回 target 包装后的对象
  • 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行; 

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
       ParameterHandler parameterHandler = 
       mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
       parameterHandler = (ParameterHandler)
      interceptorChain.pluginAll(parameterHandler);

      return parameterHandler;
}

public Object pluginAll(Object target) {
       for (Interceptor interceptor : interceptors) {
       target = interceptor.plugin(target);
       }
  return target;
}

interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象 。

3.3 简单应用

如果我们想要拦截Executor的update方法,那么可以这样定义插件:
 

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

    //这里是每次执行操作的时候,都会进行这个拦截器的方法内
    Override
   public Object intercept(Invocation invocation) throws Throwable {
          //增强逻辑
          System.out.println("对方法进行了增强....");
         return invocation.proceed(); //执行原方法

 }

 除此之外,我们还需将插件配置到sqlMapConfig.xm l中。

<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
</plugin>
</plugins>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值