Mybatis的插件机制解析

Mybatis的插件机制解析

Mybatis提供了插件机制,让我们有机会拦截其执行过程,插入我们自定义的逻辑,比如分页、SQL执行性能监控、数据库表公共字段赋值等。

学习Mybaits的插件原理,最好的方式是首先来写一个简单的插件,这个插件没什么作用,就是往控制台输出点日志。

非常简单的LogPlugin插件

Mybatis的插件都必须实现Interceptor接口,它定义了3个方法如下:

public interface Interceptor {
	//拦截方法,拦截逻辑写在这里面
  Object intercept(Invocation invocation) throws Throwable;

	//包装目标对象方法,通过代理返回包装后的代理对象
  Object plugin(Object target);

	//插件初始化,调用该方法设置配置的属性。
  void setProperties(Properties properties);

}

LogPluginV1的实现,可以看到它只实现了plugin方法,没有实现intercept方法:

public class LogPluginV1 implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return null;
    }

    @Override
    public Object plugin(final Object target) {
        //只拦截Executor类型
        if(Executor.class.isAssignableFrom(target.getClass())){
            System.out.println("----调用LogPlugin.plugin方法,对目标对象["+target+"]进行包装---");
            //返回target的动态代理对象
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Executor.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("----调用目标方法:"+target.getClass().getSimpleName() +  "." + method.getName()+"执行前打印---");
                    Object result = method.invoke(target, args);
                    System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + method.getName()+"执行后打印---");
                    return result;
                }
            });
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

将LogPluginV1插件配置到Mybatis的主配置文件中:

<plugins>
    <plugin interceptor="com.zyy.demo.plugin.LogPluginV1"/>
</plugins>

跑下我们的测试用例(简单的SelectById查询),可以看到类似的输出:

----调用LogPlugin.plugin方法,对目标对象[org.apache.ibatis.executor.CachingExecutor@5f3a4b84]进行包装---
----调用目标方法:CachingExecutor.query执行前打印---
....
----调用目标方法:CachingExecutor.query执行后打印---
----调用目标方法:CachingExecutor.close执行前打印---
----调用目标方法:CachingExecutor.close执行后打印---

可以看出,我们的插件以及其作用了,它对CachingExecutor@5f3a4b84这个对象进行包装,并拦截了其中的query和close方法,执行了我们的输出逻辑。

那么Mybatis是如何做到的呢?

Mybatis插件原理

首先,Mybatis要让我们自定义的插件生效,第一步就是将自定义的插件配置解析出来,在之前的文章 Mybatis初始化源码解析(https://blog.csdn.net/gruelxsp/article/details/103803744) 中,详细的阐述了Mybatis的配置解析,这儿简单的回顾下对插件的解析,XMLConfigBuilder#pluginElement方法:

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
  	//遍历plugins标签下配置的所有plugin
    for (XNode child : parent.getChildren()) {
    	//获取interceptor属性,即插件类的全路径
      String interceptor = child.getStringAttribute("interceptor");
      //获取对应的配置属性
      Properties properties = child.getChildrenAsProperties();
     	//通过反射实例化插件对象,注意是通过调用默认的无参构造方法来实例化的,因此如果插件没有无参构造方法,会抛出实例化错误。
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      //调用自定义插件的setProperties方法,来设置自定义插件的属性。(当然你可以在里面实现额外的逻辑)
      interceptorInstance.setProperties(properties);
      //将创建好的创建实例报错到configuration的interceptorChain中。
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

接下来,需要将插件应用到我们想要拦截的对象上去,才能执行到我们插件的代码。Mybatis会在4个地方进行插件应用,Mybatis的详细的执行流程解析 见 文章 https://blog.csdn.net/gruelxsp/article/details/103813154,这儿就直接给出4个插件应用点:

第一个是Configuration#newExecutor方法:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  ...
  //按插件加载顺序应用上所有插件,先配置的先应用,后配置的后应用。
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

第二个是Configuration#newStatementHandler方法:

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  //应用插件
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

第三个是Configuration#newParameterHandler方法:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
   //应用插件
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

第四个是Configuration#newResultSetHandler方法:

ublic ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
    ResultHandler resultHandler, BoundSql boundSql) {
  ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    //应用插件
  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  return resultSetHandler;
}

而interceptorChain.pluginAll方法非常简单,就是依次调用插件的plugin方法:

public Object pluginAll(Object target) {
	//依次调用插件的plugin方法,对目标对象完成一层一层的包装(就像洋葱一样,如果你愿意一层一层的剥开我的心....)
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

现在回过头看下LogPluginV1的plugin方法,它会在上面提到4处都会执行一次,但只有在第一处,目标对象是Executor类型时,才进行了动态代理,其他三处没做包装,返回的就是原对象,因此我们也只有调用到Executor的方法时才会被拦截,执行我们的日志输出。至于怎么调用到 的方法的(比如示例中的query和close)请参考 Mybatis的详细的执行流程解析(https://blog.csdn.net/gruelxsp/article/details/103813154)

Mybatis的Plugin工具

也许你会感到有点奇怪,在Interceptor接口中定义了Object intercept(Invocation invocation)方法,而我们的LogPluginV1插件根本没有实现它,而且从前面的源码分析中,也根本没有看到这个方法的身影,难道它是一个无用的方法?

为了说清楚这个方法的作用,我们来实现LogPluginV2版本的插件,作用和V1版本的一样,只不过实现方式略有不同:

//通过@Intercepts注解来指明,要拦截的是哪种类型的哪个方法。
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class LogPluginV2 implements Interceptor {
	
		//实现拦截逻辑
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();

        System.out.println("----调用目标方法:"+target.getClass().getSimpleName() +  "." + invocation.getMethod().getName()+"执行前打印---");
        Object result = invocation.proceed();
        System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + invocation.getMethod().getName()+"执行后打印---");
        return result;

    }
	
    @Override
    public Object plugin(final Object target) {
    		//通过Mybatis提供的Plugin工具类来完成目标对象的包装。
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

相比于V1,V2版本使用Plugin工具类来辅助我们完成插件的开发,将包装目标对象的代码和自定义逻辑的代码分离,这样让代码更清爽,这儿就将intercept方法给用上了。

Plugin类并不神秘,相反还非常简单,一起看下源码:

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private 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;
  }
	
	//这个静态方法用于对目标对象的包装,通常在我们的插件中固定写法`Plugin.wrap(target, this);`
  public static Object wrap(Object target, Interceptor interceptor) {
  	//根据自定义插件的@Intercepts注解,解析出要拦截的类型以及对应的方法。
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //获取目标对象的所有会代理的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
    	//如果有需要代理的接口,返回动态代理对象,对目标对象进行包装。注意Plugin本身实现了InvocationHandler接口。
      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 {
    	//当前类型声明要拦截的方法集合,如果当前方法在这个集合中,则调用插件的intercept方法
      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);
    }
  }

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  	//获取@Intercepts注解对象
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //使用Plugin.wrap方法进行包装时,必须要有@Intercepts注解
    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>>();
    //遍历注解的全部签名,构建出key为类型、value为Method集合的map结构。
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
      	//注意,注解是类型class的getMethod方法,传入方法名和参数类型,必须要获取到对应的方法,否则会抛出异常
        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) {
    	//当前类的所有接口,如果在注解中申明要拦截,这加入list。
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

可以看出有了Plugin工具,插件的开发非常简单了,而且可以注解声明的方式来指定想拦截什么方法就拦截什么方法,而不是像V1版本哪样写在代码逻辑里面。

插件总结

最后,简单的总结下,Mybatis的插件原理其实非常简单,就是对Mybatis执行流程中用到的4个核心类型(Executor,StatementHander、ParameterHandler、ResultSetHandler)生成代理对象,从而拦截4个核心类型的方法,实现自定义逻辑。

Mybatis还非常贴心的为我们提供了Plugin工具,通过注解的方式指定要拦截的类型和方法签名,进一步简化插件的开发,非常的轻巧,方便和实用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值