MyBatis插件原理----从<plugins>解析开始

本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间,本文我以一个例子说明了MyBatis插件是什么以及如何实现。由于MyBatis的插件已经深入到了MyBatis底层代码,因此要更好地使用插件,必须对插件实现原理及MyBatis底层代码有所熟悉才行,本文分析一下MyBatis的插件实现原理。

首先,我们从插件<plugins>解析开始,源码位于XMLConfigBuilder的pluginElement方法中:

 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).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}

这里拿<plugin>标签中的interceptor属性,这是自定义的拦截器的全路径,第6行的代码通过反射生成拦截器实例。

再拿<plugin>标签下的所有<property>标签,解析name和value属性成为一个Properties,将Properties设置到拦截器中。

最后,通过第8行的代码将拦截器设置到Configuration中,源码实现为:

 public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}

InterceptorChain是一个拦截器链,存储了所有定义的拦截器以及相关的几个操作的方法:

 public class InterceptorChain {

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

   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<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }

分别有添加拦截器、为目标对象添加所有拦截器、获取当前所有拦截器三个方法。

MyBatis插件原理----pluginAll方法添加插件

上面我们在InterceptorChain中看到了一个pluginAll方法,pluginAll方法为目标对象生成代理,之后目标对象调用方法的时候走的不是原方法而是代理方法,这个在后面会说明。

MyBatis官网文档有说明,在以下四个代码执行点上允许使用插件:

为之生成插件的时机(换句话说就是pluginAll方法调用的时机)是Executor、ParameterHandler、ResultSetHandler、StatementHandler四个接口实现类生成的时候,每个接口实现类在MyBatis中生成的时机是不一样的,这个就不看它们是在什么时候生成的了,每个开发工具我相信都有快捷键可以看到pluginAll方法调用的地方,我使用的Eclipse就是Ctrl+Alt+H。

再看pluginAll方法:

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

这里值得注意的是:

  1. 形参Object target,这个是Executor、ParameterHandler、ResultSetHandler、StatementHandler接口的实现类,换句话说,plugin方法是要为Executor、ParameterHandler、ResultSetHandler、StatementHandler的实现类生成代理,从而在调用这几个类的方法的时候,其实调用的是InvocationHandler的invoke方法
  2. 这里的target是通过for循环不断赋值的,也就是说如果有多个拦截器,那么如果我用P表示代理,生成第一次代理为P(target),生成第二次代理为P(P(target)),生成第三次代理为P(P(P(target))),不断嵌套下去,这就得到一个重要的结论:<plugins>...</plugins>中后定义的<plugin>实际其拦截器方法先被执行,因为根据这段代码来看,后定义的<plugin>代理实际后生成,包装了先生成的代理,自然其代理方法也先执行

plugin方法中调用MyBatis提供的现成的生成代理的方法Plugin.wrap(Object target, Interceptor interceptor),接着我们看下wrap方法的源码实现。

MyBatis插件原理----Plugin的wrap方法的实现

Plugin的wrap方法实现为:

 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) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

首先看一下第2行的代码,获取Interceptor上定义的所有方法签名:

 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;
}

看到先拿@Intercepts注解,如果没有定义@Intercepts注解,抛出异常,这意味着使用MyBatis的插件,必须使用注解方式

接着拿到@Intercepts注解下的所有@Signature注解,获取其type属性(表示具体某个接口),再根据method与args两个属性去type下找方法签名一致的方法Method(如果没有方法签名一致的就抛出异常,此签名的方法在该接口下找不到),能找到的话key=type,value=Set<Method>,添加到signatureMap中,构建出一个方法签名映射。举个例子来说,就是我定义的@Intercepts注解,Executor下我要拦截的所有Method、StatementHandler下我要拦截的所有Method。

回过头继续看wrap方法,在拿到方法签名映射后,调用getAllInterfaces方法,传入的是Target的Class对象以及之前获取到的方法签名映射:

 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()]);
}

这里获取Target的所有接口,如果方法签名映射中有这个接口,那么添加到interfaces中,这是一个Set,最终将Set转换为数组返回。

wrap方法的最后一步:

 if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;

如果当前传入的Target的接口中有@Intercepts注解中定义的接口,那么为之生成代理,否则原Target返回。

这段理论可能大家会看得有点云里雾里,我这里举个例子:

就以SqlCostPlugin为例,我的@Intercepts定义的是:
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,
method = "update", args = {Statement.class})}) 此时,生成的方法签名映射signatureMap应当是(我这里把Map给toString()了):
{interface org.apache.ibatis.executor.statement.StatementHandler=[public abstract int org.apache.ibatis.executor.statement.StatementHandler.update(java.sql.
Statement) throws java.sql.SQLException, public abstract java.util.List org.apache.ibatis.executor.statement.StatementHandler.query(java.sql.Statement,org.apache.
ibatis.session.ResultHandler) throws java.sql.SQLException]}
一个Class对应一个Set,Class为StatementHandler.class,Set为StataementHandler中的两个方法 如果我new的是StatementHandler接口的实现类,那么可以为之生成代理,因为signatureMap中的key有StatementHandler这个接口 如果我new的是Executor接口的实现类,那么直接会把Executor接口的实现类原样返回,因为signatureMap中的key并没有Executor这个接口

相信这么解释大家应该会明白一点。注意这里生不生成代理,只和接口在不在@Intercepts中定义过有关,和方法签名无关,具体某个方法走拦截器,在invoke方法中,马上来看一下。

MyBatis插件原理----Plugin的invoke方法

首先看一下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是InvocationHandler接口的实现类,换句话说,为目标接口生成代理之后,最终执行的都是Plugin的invoke方法,看一下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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

在这里,将method对应的Class拿出来,获取该Class中有哪些方法签名,换句话说就是Executor、ParameterHandler、ResultSetHandler、StatementHandler,在@Intercepts注解中定义了要拦截哪些方法签名。

如果当前调用的方法的方法签名在方法签名集合中,即满足第4行的判断,那么调用拦截器的intercept方法,否则方法原样调用,不会执行拦截器。

【MyBatis源码分析】插件实现原理的更多相关文章

  1. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

  2. 精尽MyBatis源码分析 - 插件机制

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  3. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  4. MyBatis 源码分析 - 内置数据源

    1.简介 本篇文章将向大家介绍 MyBatis 内置数据源的实现逻辑.搞懂这些数据源的实现,可使大家对数据源有更深入的认识.同时在配置这些数据源时,也会更清楚每种属性的意义和用途.因此,如果大家想知其 ...

  5. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  6. MyBatis 源码分析 - 配置文件解析过程

    * 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...

  7. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  8. 精尽MyBatis源码分析 - 文章导读

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  9. MyBatis源码分析(2)—— Plugin原理

    @(MyBatis)[Plugin] MyBatis源码分析--Plugin原理 Plugin原理 Plugin的实现采用了Java的动态代理,应用了责任链设计模式 InterceptorChain ...

  10. Mybatis源码分析--关联表查询及延迟加载原理(二)

    在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...

随机推荐

  1. MySQL select语句直接导出数据

    select * into outfile '文件存放路径' from 表名; (先记下来,还未测试)

  2. backgroud背景色样式兼容ie8 rgba()用法

    今天遇到了一个问题,要在一个页面中设置一个半透明的白色div.这个貌似不是难题,只需要给这个div设置如下的属性即可: background: rgba(255,255,255,.1);但是要兼容到i ...

  3. MySql主键自动生成,表、实体、C#调用方法

    1.表:mysql建表语句 DROP TABLE IF EXISTS `keycode`; CREATE TABLE `keycode` ( `Id` ) NOT NULL AUTO_INCREMEN ...

  4. Mysql数据库建立索引的优缺点有哪些?

    索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息. 什么是索引 数据库索引好比是一本书前面的目录,能加快数据库的查询速度. 例如这样一个查询:select * ...

  5. Asp.Net Core 2.1+的视图缓存(响应缓存)

    响应缓存Razor 页与 ASP.NET 核心 2.0 中不支持. 此功能将支持ASP.NET 核心 2.1 版本. 在老的版本的MVC里面,有一种可以缓存视图的特性(OutputCache),可以保 ...

  6. Spring data mongodb 替换 Repository 实现类,findAll 排除 字段

    因文档比较大,有时候findAll 不想返回所有数据.没有找到默认的findAll 能够include 或者 exclude 的方法,所以想办法扩展一下实现类 query.fields().inclu ...

  7. java:替换字符串中的ASCII码

    可对照查看网盘ASCII表http://yunpan.cn/cyxg4wQjQaGEQ (提取码:8b29) public static void main(String[] args) { // / ...

  8. linux kvm虚拟机快速构建及磁盘类型

    KVM命令管理 virsh命令:用来管理各虚拟机的接口命令查看/创建/停止/关闭...支持交互模式格式:virsh 控制指令 [虚拟机名称] [参数] [root@room1pc01 桌面]# vir ...

  9. @restcontroller与@controller的区别

    这段时间偷偷看了下spring boot.结果引用模板时没注意,把@restcontroller替换了@controlle,结果模板出不来.终究原因是spring的知识不到位. 下面说说这2的说明和区 ...

  10. FusionCharts饼图中label值太长怎么解决

    FusionCharts饼图中label值太长怎么解决 1.使用hoverText属性 <?xml version="1.0" encoding="UTF-8&qu ...