浅谈myBatis中的插件机制

插件的配置与使用

在mybatis-config.xml配置文件中配置plugin结点,比如配置一个自定义的日志插件LogInterceptor和一个开源的分页插件PageInterceptor:

<plugins>
  <plugin interceptor="com.crx.plugindemo.LogInterceptor"></plugin>
  <plugin interceptor="com.github.pagehelper.PageInterceptor">
    <property name="helperDialect" value="oracle" />
  </plugin>
</plugins>

插件的工作原理

借助责任链模式,定义一系列的过滤器,在查询等方法执行时进行过滤,从而达到控制参数、调整查询语句和控制查询结果等作用。下面从插件的加载(初始化)、注册和调用这三个方面阐述插件的工作原理。

过滤器的加载(初始化)

和其他配置信息一样,过滤器的加载也会在myBatis读取配置文件创建Configuration对象时进行,相应的信息存储在Configuration的interceptorChain属性中,InterceptorChain封装了一个包含Interceptor的list:

private final List<Interceptor> interceptors = new ArrayList<>();

在XMLConfigBuilder进行解析配置文件时执行pluginElement方法,生成过滤器实例,并添加到上述list中:

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor()
        .newInstance();
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

过滤器的注册

可以为Executor、ParameterHandler、ResultSetHandler和StatementHandler四个接口注册过滤器,注册的时机也就是这四种接口的实现类的对象的生成时机,比如Executor的过滤器的注册发生在SqlSessionFactory使用openSession方法构建SqlSession的过程中(因为SqlSession依赖一个Executor实例),ParameterHandler和StatementHandler的过滤器发生在doQuery等sql执行方法执行时注册,而ResultHandler的过滤器的注册则发生在查询结果返回给客户端的过程中。以Executor的过滤器的注册为例,经过了这样的过程:

现在详细的分析一下Plugin的wrap这个静态的包装方法:

public static Object wrap(Object target, Interceptor interceptor) {
  // 从定义的Interceptor实现类上的注解读取需要拦截的类、方法
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // Executor、ParameterHandler、ResultSetHandler、StatementHandler
  Class<?> type = target.getClass();
  // 从当前执行的目标类中进行匹配,过滤出符合当前目标的的过滤器
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 动态代理生成Executor的代理实例
    return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
                   new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

上述代码中的getSignatureMap方法是解析Interceptor上面的注解的过程,从注解中读取出需要拦截的方法,依据@Signature的三个变量类、方法method和参数args就能通过反射唯一的定位一个需要拦截的方法。

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
  if (interceptsAnnotation == null) {
    throw new PluginException(
      "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
  }
  Signature[] sigs = interceptsAnnotation.value();
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  for (Signature sig : sigs) {
    Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
    try {
      Method method = sig.type().getMethod(sig.method(), sig.args());
      methods.add(method);
    } catch (NoSuchMethodException e) {
      throw new PluginException(
        "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
    }
  }
  return signatureMap;
}

而getAllInterfaces方法是依据不同的目标对象(Executor等四种)进行过滤的过程,只给对应的目标进行注册:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  Set<Class<?>> interfaces = new HashSet<>();
  while (type != null) {
    for (Class<?> c : type.getInterfaces()) {
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  return interfaces.toArray(new Class<?>[interfaces.size()]);
}

至此,实际使用的Executor对象将是通过动态代理生成的Plugin实例。

过滤器的调用

在第二步中完成了过滤器的注册,在实际调用Executor时,将由实现了InvocationHandler接口的Plugin实例进行接管,对Executor相应方法方法的调用,将实际上调用动态代理体系下的invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      Object result=interceptor.intercept(new Invocation(target, method, args));
      return result;
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

如前所述,插件的工作原理是基于责任链模式,可以注册多个过滤器,层层包装,最终由内而外形成了一个近似装饰器模式的责任链,最里面的基本实现是CachingExecutor:

从InterceptorChain的pluginAll方法可以看出这个结构的构造过程:

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    // 从这可以看出过滤器的传递的过程:动态代理实例由内而外层层包装,类似于与装饰器的结构,基础			 实现是一个Executor
    target = interceptor.plugin(target);
  }
  return target;
}

这种由内而外的包装的栈结构从外向内层层代理调用,完成了责任链任务的逐级推送。从这个注册过程可以看到,在list中越前面的Interceptor越先被代理,在栈结构中越处于底层,执行的顺序越靠后。造成了注册顺序和执行顺序相反的现象。

插件的典型案例PageHelper

pagehelper是一个实现物理分页效果的开源插件,并且在底层通过Dialect类适配了不同的数据库,其主要作用是拦截sql查询,构造一个查询总数的新的以"_COUNT"结尾的新sql,最终再进行分页查询。

自定义插件

定义Interceptor接口的实现类并在其上使用@Intercepts和@Signature注解进行过滤的类和方法,比如定义一个打日志的插件:

@Intercepts({
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class }),
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }), })
public class LogInterceptor implements Interceptor {
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		System.out.println("进入了自定义的插件过滤器!");
		System.out.println("执行的目标是:" + invocation.getTarget());
		System.out.println("执行的方法是:" + invocation.getMethod());
		System.out.println("执行的参数是:" + invocation.getArgs());
		return invocation.proceed();
	}
}

@Intercepts注解中包含了一个方法签名数组,即@Signature数组,@Signature有三个属性,type、method和args分别定义要拦截的类、方法名和参数,这样就可以通过反射唯一的确定了要拦截的方法。type即为在工作原理分析中提到的Executor、ParameterHandler、ResultSetHandler和StatementHandler,method配置对应接口中的方法。

到此这篇关于浅谈myBatis中的插件机制的文章就介绍到这了,更多相关myBatis 插件机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • MyBatis Plus插件机制与执行流程原理分析详解

    MyBatis Plus插件 MyBatis Plus提供了分页插件PaginationInterceptor.执行分析插件SqlExplainInterceptor.性能分析插件PerformanceInterceptor以及乐观锁插件OptimisticLockerInterceptor. Mybatis 通过插件 (Interceptor) 可以做到拦截四大对象相关方法的执行 ,根据需求完成相关数据的动态改变. 四大对象是: Executor StatementHandler Parame

  • mybatis的插件机制详解

    前言 Mybatis作为一个应用广泛的优秀的ORM框架,已经成了JavaWeb世界近乎标配的部分,这个框架具有强大的灵活性,在四大组件(Executor.StatementHandler.ParameterHandler.ResultSetHandler)处提供了简单易用的插件扩展机制.Mybatis对持久层的操作就是借助于四大核心对象.MyBatis支持用插件对四大核心对象进行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说

  • 浅谈myBatis中的插件机制

    插件的配置与使用 在mybatis-config.xml配置文件中配置plugin结点,比如配置一个自定义的日志插件LogInterceptor和一个开源的分页插件PageInterceptor: <plugins> <plugin interceptor="com.crx.plugindemo.LogInterceptor"></plugin> <plugin interceptor="com.github.pagehelper.P

  • 浅谈mybatis中的#和$的区别 以及防止sql注入的方法

    mybatis中的#和$的区别 1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id,  如果传入的

  • 浅谈mybatis中的#和$的区别

    1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为ord

  • 浅谈mybatis中SQL语句给boolean类型赋值问题

    我就废话不多说了,大家还是直接看代码吧~ <select id="getBiTree" parameterType="String" resultMap="MenuVoListMap"> SELECT m.menu_id , m.parent_id , m.`name` , 1 opens FROM menu m WHERE m.is_valid = 1 AND (m.type = 0 or m.type = 1) and m.men

  • 浅谈Mybatis中resultType为hashmap的情况

    现在有一张user表 id ,name,age 我们进行一个简单的查询: <select id="test" resultType="Uer"> select id ,name,age from user </select> 查询完后,怎么去接收这个查询结果呢,通常在这个mapper.xml对应的接口中使用List<User>做为返回值去接收,最后存储的样子就是下面的图 这是一个很简单的单表查询操作,其实这种简单的单表查询操作不需

  • 浅谈Mybatis乐观锁插件

    背景:对于数据库的同一条记录,假如有两个人同时对数据进行了修改,然后最终同步到数据库的时候,因为存在着并发,产生的结果是不可预料的.最简单的解决方式就是通过给表的记录加一个version字段,记录在修改的时候需要比较一下version是否匹配,如果匹配就更新,不匹配就直接失败.更新成功则把version+1,也就是所谓的乐观锁.当然这样的逻辑最好能做到对开发人员透明,本插件就是来做这件事情的. 1. 使用方式:在mybatis配置文件中加入如下配置,就完成了. <plugins> <pl

  • 浅谈MyBatis中@MapKey的妙用

    目录 MyBatis @MapKey的妙用 背景 实现 源码分析 思考 Mybatis @MapKey分析 1. MapKey注解有啥功能 2. MapKey的源码分析 1. MapperMethod对MapKey的操作 2. DefaultMapResultHandler是什么 MyBatis @MapKey的妙用 背景 在实际开发中,有一些场景需要我们返回主键或者唯一键为Key.Entity为Value的Map集合,如Map<Long, User>,之后我们就可以直接通过map.get(k

  • 浅谈Mybatis分页插件,自定义分页的坑

    场景:PageHelper 的默认分页方案是 select count(0) from (你的sql) table_count 由于查询数据比较大时,导致分页查询效率低下. 优化:使用自定义的count查询.. 废话不多说,对应代码如下: 这个时候会使用自定义的 count sql进行统计查询. 然后一般分页默认使用 PageHelper.startPage(); 作者优化:如果获取的数量大于实际数量,则进行pageNum优化. 所以 最好建议重载 startPage. 不进行优化!!! 要不然

  • 浅谈mybatis mapper.xml文件中$和#的区别

    #{}表示一个占位符即?,可以有效防止sql注入.在使用时不需要关心参数值的类型,mybatis会自动进行java类型和jdbc类型的转换. #{}可以接收简单类型值或pojo属性值,如果传入简单类型值,#{}括号中可以是任意名称. <!-- 根据名称模糊查询用户信息 --> <select id="findUserById" parameterType="String" resultType="user"> select

  • 浅谈Redis 中的过期删除策略和内存淘汰机制

    目录 前言 Redis 中 key 的过期删除策略 1.定时删除 2.惰性删除 3.定期删除 Redis 中过期删除策略 从库是否会脏读主库创建的过期键 内存淘汰机制 内存淘汰触发的最大内存 有哪些内存淘汰策略 内存淘汰算法 LRU LFU 为什么数据删除后内存占用还是很高 内存碎片如何产生 碎片率的意义 如何清理内存碎片 总结 参考 前言 Redis 中的 key 设置一个过期时间,在过期时间到的时候,Redis 是如何清除这个 key 的呢? 这来分析下 Redis 中的过期删除策略和内存淘

随机推荐