MyBatis插件原理解析及自定义插件实践

Posted 敲代码的小小酥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis插件原理解析及自定义插件实践相关的知识,希望对你有一定的参考价值。

一、插件原理解析

首先,要搞清楚插件的作用。不管是我们自定义插件,还是用其他人开发好的第三方插件,插件都是对MyBatis的四大核心组件:Executor,StatementHandler,ParameterHandler,ResultSetHandler来进行增强的,利用动态代理的技术,来增强框架的方法,来满足我们特殊的业务需求。

1.先看几个重要的类:

package org.apache.ibatis.plugin;

import java.util.Properties;

/**
 * @author Clinton Begin
 */
 /**
  所有的插件,都要实现这个接口。
*/
public interface Interceptor 

//这个方法写增强逻辑
  Object intercept(Invocation invocation) throws Throwable;

//生成代理对象,生成四大核心组件的代理类。通过Plugin.warp(target,this)来生成
  Object plugin(Object target);
//读取MyBatis配置文件中定义插件拦截器时配置的一些属性
  void setProperties(Properties properties);



再看intercept方法中参数对象Invocation源码:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Clinton Begin
 */
public class Invocation 
  //要生成代理的对象
  private final Object target;
  //目标方法,即要增强的方法
  private final Method method;
  //增强方法的参数
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) 
    this.target = target;
    this.method = method;
    this.args = args;
  

  public Object getTarget() 
    return target;
  

  public Method getMethod() 
    return method;
  

  public Object[] getArgs() 
    return args;
  

  //调用目标方法本身
  public Object proceed() throws InvocationTargetException, IllegalAccessException 
    return method.invoke(target, args);
  


上面提到,插件使用的是jdk动态代理技术,MyBatis定义了Plugin类实现了InvocationHandler接口,看源码:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler 

  private final Object target;
  private final Interceptor interceptor;
  //解析Interceptor插件实现类的@Signature注解
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) 
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  

//生成代理对象
  public static Object wrap(Object target, Interceptor interceptor) 
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) 
    //JDK动态代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    
    return target;
  

//执行代理对象的代理方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
    try 
    //获取@Signature注解中method的值
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) 
      //命中的方法,执行插件实现类的加强方法
        return interceptor.intercept(new Invocation(target, method, args));
      
      //方法没命中,执行原方法
      return method.invoke(target, args);
     catch (Exception e) 
      throw ExceptionUtil.unwrapThrowable(e);
    
  
//解析@Signature参数
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) 
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    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<Class<?>, Set<Method>>();
    for (Signature sig : sigs) 
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) 
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      
      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;
  

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) 
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    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()]);
  


可见,Plugin类的wrap方法生成代理类,invoke方法执行代理方法。Plugin中有解析@Signature,看@Signature的源码:

package org.apache.ibatis.plugin;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Clinton Begin
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target()
public @interface Signature 
//四大核心组件类中的一个,定义哪个类,那么插件就是要加强哪个类的方法
  Class<?> type();
//要拦截的方法,type定义的哪个组件,这里就要写该组件里对应的要加强的方法
  String method();
 //method所写方法的参数,可以定位到重载方法的某一个
  Class<?>[] args();

2.开发插件基本套路
介绍了插件有关的几个重要的类,下面就用这几个类,演示一下插件开发的基本套路。
首先,定义插件类,实现Interceptor接口,并定义@Signature 说明要拦截的类和方法,如下:

@Intercepts(
        @Signature(
                type = Executor.class,//拦截Executor类
                method = "update",//拦截update方法
                args = MappedStatement.class,Object.class //update方法的参数
        )
)
public class MyBatisPlugin implements Interceptor 
    @Override
    public Object intercept(Invocation invocation) throws Throwable 
        System.out.println("拦截的目标对象:"+invocation.getTarget());
        System.out.println("写加强逻辑");
        return invocation.proceed();//执行原方法
    

    @Override
    public Object plugin(Object target) 
        //生成代理对象
        return Plugin.wrap(target,this);
    

    @Override
    public void setProperties(Properties properties) 
        System.out.println("获取配置文件参数");
    

然后,将该插件注入Spring容器中,即可。如果是spring项目,可以在xml中定义plugins插件,如下:

 <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
    </plugins>

如果是SpringBoot项目,如下注入:

@Configuration
public class MybatisPluginsBean 
    @Bean
    public MyBatisPlugin myBatisPlugin()
        return new MyBatisPlugin();
    

由上可见,定义插件只需要两个关键步骤,即定义Interceptor实现类,实现插件逻辑;注入到spring容器中这两步即可。

二、PageHelper插件原理解析

我们结合PageHelper插件,研究一下插件的执行原理。
首先,读取xml文件,并解析plugins标签,然后将配置的interceptor类通过反射生成对象,注入到Spring容器中。这个步骤的代码我们省去,下面看生成插件对象后,执行了哪些操作。
首先,生成插件对象后,在Configuration类中,将插件对象加入到了拦截链interceptorChain中:

然后看一下,PageInterceptor类,是对Executor组件的query方法进行了拦截:

那么何时对Executor类进行的增强呢,我们知道,插件的原理是代理,那么代理对象何时生成呢,一定是在创建Executor对象的时候,生成代理对象,总不能先生成元对象,执行了一半儿,再生成代理对象吧,那容器中岂不是有了两个Executor对象,破坏了单例规则吗,对吧。所以看Executor创建的地方:
Executor是在SqlSession创建的时候被创建的,SqlSession是在SqlSessionFactory中的openSession方法中创建的,所以从openSession中点进去,查看,最终点到:


看pluginAll方法:

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

可见,遍历拦截链,然后调用plugin方法,获得代理对象。interceptor的plugin最终会调用Plugin的wrap方法,该方法在第一部分已经分析过,最终,Executor就返回了代理对象。
那么在接下来执行Executor方法的时候,就会调用Plugin类的invoke方法,如果是加强的方法,则走PageInterceptor的intercept方法,进行分页增强,如果没有拦截的方法,则执行原方法即可。这就是分页插件的执行流程。
**总结:**由上面的流程分析可以知道,插件对象是在四大组件的类生成时,生成的代理对象。通过Configuration中的拦截链属性,判断是否要生成代理对象。而代理对象方法执行,由Plugin类来完成。我们只需定义Interceptor的实现类,来做逻辑代码,而插件的原生方法还是加强方法的执行,是由Plugin类支撑的,也就是Plugin是Interceptor的底层实现。

三、自定义插件实践

1.屏蔽敏感字插件实现
在某些业务场景中,涉及到一些敏感字,需要用*号代替。比如身份证号,手机号中间部分,都要用星号代替,而不是展示全部。我们正常的实现思路是查询出数据后,再做处理,把有身份证号的模块做一下处理,再把有手机号的模块做一下处理。假如这种模块很多的话,那我们需要改很多代码。
这个问题,就可以用插件的方式解决。屏蔽关键字,是对查询结果的处理,在MyBatis中对应的就是ResultSetHandler组件。用插件生成ResultSetHandler的代理对象,在ResultSet中对字段进行屏蔽,不就避免了按功能模块进行屏蔽的繁琐了吗 。
实现思路:
对于开发插件而言,只需关注两步,1.实现Interceptor接口,写插件逻辑 2.注册插件。
首先,拦截ResultSetHandler类的handleResultSets方法,该方法对返回值进行处理。

@Intercepts(
        @Signature(
                type = ResultSetHandler.class,
                method = "handleResultSets",
                args = Statement.class 
        )
)
public class MyBatisPlugin implements Interceptor 
    @Override
    public Object intercept(Invocation invocation) throws Throwable 
        //执行原方法
        List<Object> records = (List<Object>)invocation.proceed();
        //TODO 对返回值records进行加密处理
        return records;//执行原方法
    

    @Override
    public Object plugin(Object target) 
        //生成代理对象
        return Plugin.wrap(target,this);
    

    @Override
    public void setProperties(Properties properties) 
        System.out.println("获取配置文件参数");
    

至于TODO部分,就是加密逻辑,这个我们自己实现即可,这里不过多描述。可以自定义注解,有这个注解的字段,就进行加密处理。
2.分表功能实现
见日常开发总结模块

以上是关于MyBatis插件原理解析及自定义插件实践的主要内容,如果未能解决你的问题,请参考以下文章

深入浅出MyBatis:MyBatis插件及开发过程

mybatis-插件体系之原理

MyBatis源码分析插件实现原理

Mybatis源码解析MyBatis插件原理

mybatis插件机制原理

Mybatis插件原理和整合Spring