千家信息网

Mybatis之插件原理

发表于:2025-01-30 作者:千家信息网编辑
千家信息网最后更新 2025年01月30日,鲁春利的工作笔记,好记性不如烂笔头转载自:深入浅出Mybatis-插件原理Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重
千家信息网最后更新 2025年01月30日Mybatis之插件原理

鲁春利的工作笔记,好记性不如烂笔头



转载自:深入浅出Mybatis-插件原理


Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。


代理链的生成

Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。


下面以Executor为例。Mybatis在创建Executor对象时:

org.apache.ibatis.session.SqlSessionFactory.openSession()    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession()        org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(execType, level, false) {            final Executor executor = configuration.newExecutor(tx, execType);        }

说明:org.apache.ibatis.builder.xml.XMLConfigBuilder类解析MyBatis的配置文件,获取其中配置的Interceptor并调用configuration.addInterceptor(interceptorInstance);配置到拦截器链中。


org.apache.ibatis.session.Configuration

package org.apache.ibatis.session;public class Configuration { protected Environment environment; protected String logPrefix;  protected Class  logImpl;  protected Class  vfsImpl;  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;  protected Set lazyLoadTriggerMethods = new HashSet(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));  protected Integer defaultStatementTimeout;  protected Integer defaultFetchSize;  // 默认的Executor  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;     // InterceptorChain里保存了所有的拦截器,它在mybatis初始化的时候创建。   protected final InterceptorChain interceptorChain = new InterceptorChain();     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);    }    if (cacheEnabled) {      executor = new CachingExecutor(executor);    }    executor = (Executor) interceptorChain.pluginAll(executor);    return executor;  }}

org.apache.ibatis.plugin.InterceptorChain

package org.apache.ibatis.plugin;/** * @author Clinton Begin */public class InterceptorChain {  private final List interceptors = new ArrayList();  /**   * 每一个拦截器对目标类都进行一次代理    * @param target    * @return 层层代理后的对象    */  public Object pluginAll(Object target) {    for (Interceptor interceptor : interceptors) {      target = interceptor.plugin(target);    }    return target;  }  public void addInterceptor(Interceptor interceptor) {    interceptors.add(interceptor);  }    public List getInterceptors() {    return Collections.unmodifiableList(interceptors);  }}

下面以一个简单的例子来看看这个plugin方法里到底发生了什么。

package com.invicme.common.persistence.interceptor;import java.sql.Connection;import java.lang.reflect.Method;import java.util.Properties;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Plugin;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/** *  * @author lucl * @version 2017-01-09 *  * 示例的分页拦截器: * 
 *         @Intercepts用于表明当前的对象是一个Interceptor *         @Signature则表明要拦截的接口(type)、方法(method)以及对应的参数类型(args) *             type: Mybatis只支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。 *             method: 这几个类的方法 *             args:   这几个类的方法的参数 * 
* */@Intercepts(value = { @Signature(type=Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type=StatementHandler.class, method="prepare", args={Connection.class}) })public class SamplePagingInterceptor implements Interceptor { private Logger logger = LoggerFactory.getLogger(getClass()); /** * 拦截方法 * Invocation中定义了一个proceed方法,其逻辑就是调用当前方法, * 如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。 */ @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); logger.info("target is {}", target.getClass().getName()); Method method = invocation.getMethod(); logger.info("method is {}", method.getName()); Object[] args = invocation.getArgs(); for (Object arg : args) { logger.info("arg is {}", arg); } Object proceed = invocation.proceed(); logger.info("proceed is {}", proceed.getClass().getName()); return proceed; } /** * 用于封装目标对象,该方法可以返回目标对象本身,也可以返回一个它的代理对象。 */ @Override public Object plugin(Object target) { logger.info("target is {}", target.getClass().getName()); return Plugin.wrap(target, this); } /** * 用于在Mybatis配置文件中指定一些属性 */ @Override public void setProperties(Properties properties) { String value = properties.getProperty("sysuser"); logger.info("value is {}", value); }}

每一个拦截器都必须实现上面的三个方法,其中:

1) Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。

2) Object plugin(Object target)就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来完成的,把目标target和拦截器this传给了包装函数。

3) setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。

注解里描述的是指定拦截方法的签名 [type, method, args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。


代理链的生成

为了更好的理解后面的内容,穿插一个Java反射的示例

package com.invicme.common.aop.proxy;import java.lang.reflect.Method;import java.lang.reflect.Type;import java.util.HashMap;import java.util.HashSet;import java.util.Iterator;import java.util.Map;import java.util.Set;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.PluginException;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.session.Configuration;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.invicme.common.persistence.interceptor.SamplePagingInterceptor;/** *  * @author lucl * @version 2017-01-18 *  * 测试Class对象 * */public class MainDriver {    private static Logger logger = LoggerFactory.getLogger(MainDriver.class);    public static void main(String[] args) {        SamplePagingInterceptor interceptor = new SamplePagingInterceptor();        wrap(interceptor);    }        public static Object wrap (SamplePagingInterceptor interceptor) {        // 获取拦截器对应的@Intercepts注解        Map, Set> signatureMap = getSignatureMap(interceptor);        {            for (Iterator> it = signatureMap.keySet().iterator(); it.hasNext(); ) {                Class clazz = it.next();                Set set = signatureMap.get(clazz);                logger.info("【Signature】clazz name is {}", clazz.getName());                for (Iterator itt = set.iterator(); itt.hasNext(); ) {                    Method method = itt.next();                    logger.info("\tmethod name is {}", method.getName());                    Type[] genericParameterTypes = method.getGenericParameterTypes();                    for (Type type : genericParameterTypes) {                        logger.info("\t\tparam is {}", type);                    }                }            }        }                logger.info("================================================================");        {            Configuration configuration = new Configuration();            Executor executor = configuration.newExecutor(null);            Class[] interfaces = executor.getClass().getInterfaces();            for (Class clazz : interfaces) {                logger.info("【Interface】clazz name is {}", clazz);            }        }                return null;    }    /**     * 获取拦截器对应的@Intercepts注解     * @param interceptor     * @return     */    private static Map, Set> 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, Set> signatureMap = new HashMap, Set>();        for (Signature sig : sigs) {            // 处理重复值的问题            Set methods = signatureMap.get(sig.type());            if (methods == null) {                methods = new HashSet();                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;    }}


继续进行Plugin代码的说明。

从前面可以看出,每个拦截器的plugin方法是通过调用Plugin.wrap方法来实现的。代码如下:

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 Object target;  private Interceptor interceptor;  private Map, Set> signatureMap;  private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) {    this.target = target;    this.interceptor = interceptor;    this.signatureMap = signatureMap;  }  public static Object wrap(Object target, Interceptor interceptor) {    // 从拦截器的注解中获取拦截的类名和方法信息      Map, Set> signatureMap = getSignatureMap(interceptor);    Class type = target.getClass();    // 解析被拦截对象的所有接口(注意是接口)      Class[] interfaces = getAllInterfaces(type, signatureMap);    if (interfaces.length > 0) {      // 生成代理对象,Plugin对象为该代理对象的InvocationHandler      //(InvocationHandler属于java代理的一个重要概念,不熟悉的请参考Java动态代理)       // http://luchunli.blog.51cto.com/2368057/1795921        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 {      Set 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);    }  }}

这个Plugin类有三个属性:

private Object target;                  // 被代理的目标类private Interceptor interceptor;             // 对应的拦截器private Map, Set> signatureMap;    // 拦截器拦截的方法缓存


关于InvocationHandler请参阅:Java动态代理


我们再次结合(Executor)interceptorChain.pluginAll(executor)这个语句来看,这个语句内部对executor执行了多次plugin(org.apache.ibatis.plugin.InterceptorChain)。

  private final List interceptors = new ArrayList();    public Object pluginAll(Object target) {    for (Interceptor interceptor : interceptors) {      target = interceptor.plugin(target);    }    return target;  }

说明:

拦截器在解析MyBatis的配置文件时已经add到了该interceptors拦截器中。

org.apache.ibatis.builder.xml.XMLConfigBuilder类解析MyBatis的配置文件,获取其中配置的Interceptor并调用configuration.addInterceptor(interceptorInstance);配置到拦截器链中。


过程分析:

第一次plugin后通过Plugin.wrap方法生成了第一个代理类,姑且就叫executorProxy1,这个代理类的target属性是该executor对象

第二次plugin后通过Plugin.wrap方法生成了第二个代理类,姑且叫executorProxy2,这个代理类的target属性是executorProxy1...

这样通过每个代理类的target属性就构成了一个代理链(从最后一个executorProxyN往前查找,通过target属性可以找到最原始的executor类)。


代理链的拦截

代理链生成后,对原始目标的方法调用都转移到代理者的invoke方法上来了。Plugin作为InvocationHandler的实现类,它(org.apache.ibatis.plugin.Plugin)的invoke方法是怎么样的呢?

  @Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {      Set methods = signatureMap.get(method.getDeclaringClass());      if (methods != null && methods.contains(method)) {        // 调用代理类所属拦截器的intercept方法        return interceptor.intercept(new Invocation(target, method, args));      }      return method.invoke(target, args);    } catch (Exception e) {      throw ExceptionUtil.unwrapThrowable(e);    }  }

invoke里,如果方法签名和拦截中的签名一致,就调用拦截器的拦截方法。我们看到传递给拦截器的是一个Invocation对象,这个对象是什么样子的,他的功能又是什么呢?

package org.apache.ibatis.plugin;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * @author Clinton Begin */public class Invocation {  private Object target;  private Method method;  private 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);  }}

可以看到,Invocation类保存了代理对象的目标类,执行的目标类方法以及传递给它的参数。

在每个拦截器的intercept方法内,最后一个语句一定是invocation.proceed()(不这么做的话拦截器链就断了,你的mybatis基本上就不能正常工作了)。invocation.proceed()只是简单的调用了下target的对应方法,如果target还是个代理,就又回到了上面的Plugin.invoke方法了。这样就形成了拦截器的调用链推进。


在MyBatis中配置的拦截器方式为:

                        

我们假设在MyBatis配置了一个插件,在运行时会发生什么?

1) 所有可能被拦截的处理类都会生成一个代理

2) 处理类代理在执行对应方法时,判断要不要执行插件中的拦截方法

3) 执行插接中的拦截方法后,推进目标的执行

如果有N个插件,就有N个代理,每个代理都要执行上面的逻辑。


0