这么强大的Mybatis插件机制原来就是这?

Mybatis开发中经常会用到pagehelper分页插件,除此之外还有慢sql上报等各种各样的插件,那么Mybatis是如何来实现如此强大的插件机制呢?一起来看看吧。

Mybatis插件机制介绍

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

执行器,提供操作数据库的接口。

  • ParameterHandler (getParameterObject, setParameters)

参数处理器,设置sql的参数。

  • ResultSetHandler (handleResultSets, handleOutputParameters)

结果集处理器,处理从数据库查询的结果集,封装成对象等。

  • StatementHandler (prepare, parameterize, batch, update, query)

语法处理器,真正去执行数据库CRUD。

他们的引用关系如下
在这里插入图片描述
可见其插件是基于方法拦截来实现的!我们姑且猜测是和AOP有关,到底对不对呢,往下看就知道了。

自定义一个Mybatis插件

自定义一个插件是如此简单,以拦截ResultSetHandler为例,仅需要两步(Mybatis版本:3.5.6):

步骤一:实现Interceptor接口,如下所示

package mybatisplugin;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import java.sql.Statement;

/**
 * @author xujian
 * 2021-05-11 15:22
 **/
@Intercepts(@Signature(type = ResultSetHandler.class,//指定你要拦截是哪个对象
        method = "handleResultSets",//指定你要拦截的是哪个方法
        args = {Statement.class}))//考虑到方法重载,还需要指定参数列表
@Slf4j
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("----插件拦截到handleResultSets----");
        //实行原始的方法调用
        return invocation.proceed();
    }
}

步骤二:在Mybatis的配置文件加入自定义插件的配置,如下所示

	<plugins>
        <plugin interceptor="mybatisplugin.MyPlugin"></plugin>
    </plugins>

这样,当执行了数据库查询操作,调用ResultSetHandler#handleResultSets封装返回结果集之前会打印日志,如下图所示
在这里插入图片描述

插件执行原理分析

插件定义

实现Interceptor,其源码如下

/**
 * 拦截器接口
 *
 * @author Clinton Begin
 */
public interface Interceptor {

    /**
     * 拦截方法,自定义插件需要实现的方法
     *
     * @param invocation 调用信息
     * @return 调用结果
     * @throws Throwable 若发生异常
     */
    Object intercept(Invocation invocation) throws Throwable;

  	/**
     * 应用插件。如应用成功,则会创建目标对象的代理对象
     *
     * @param target 目标对象
     * @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
     */
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

 		/**
     * 设置插件属性
     *
     * @param <plugin>标签的properties 属性
     */
    default void setProperties(Properties properties) {
    }
}

插件是定义好了,那插件是在什么时候生效的呢?

插件初始化

当在配置文件配置上自定义插件以后,mybatis在初始化解析配置文件的时候,就会解析标签。

XMLConfigBuilder#parseConfiguration

private void parseConfiguration(XNode root) {
        try {
            ...
            // 解析 <plugins /> 标签
            pluginElement(root.evalNode("plugins"));
            ...
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

去到真正解析标签的方法XMLConfigBuilder#pluginElement

private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            // 遍历 <plugins /> 标签
            for (XNode child : parent.getChildren()) {
              	//从xml的interceptor属性中解析出插件的名称(包名+类名即全限定名)
                String interceptor = child.getStringAttribute("interceptor");
              	//1、从xml配置文件解析出来插件的配置
                Properties properties = child.getChildrenAsProperties();
                //2、创建 Interceptor 对象,并设置属性
                Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
              	//3、给插件对象设置上插件配置的属性
                interceptorInstance.setProperties(properties);
                //4、添加到 configuration 中
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }

注意到上面的第2步,该步骤是通过反射创建出插件对象的实例,也就是MyPlugin对象。

跟踪resolveClass方法,最后发现实际上是调用的TypeAliasRegistry#resolveAlias

具体来看代码

/**
*参数是插件的全限定名
返回插件的Class对象
**/
public <T> Class<T> resolveAlias(String string) {
        try {
            if (string == null) {
                return null;
            }
            // issue #748
            // 转换成小写
            String key = string.toLowerCase(Locale.ENGLISH);
            Class<T> value;
            if (TYPE_ALIASES.containsKey(key)) {
            //首先,从 TYPE_ALIASES(别名注册表) 中获取,如果配置了别名,
          	//那么配置插件的时候就不需要配置全限定名,只需要配置类名即可,
          	//那么就会走到该分支
                value = (Class<T>) TYPE_ALIASES.get(key);
            // 其次,直接获得对应类
            } else {
                value = (Class<T>) Resources.classForName(string);
            }
            return value;
        } catch (ClassNotFoundException e) { // 异常
            throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
        }
    }

经过上述过程就拿到了插件的实例对象,然后走到步骤4,该步骤是将插件设置到全局配置类Configuration中。

跟踪addInterceptor方法,发现其最终调用的是InterceptorChain#addInterceptor,而Configuration中就持有InterceptorChain的引用。

/**
 * 拦截器链
 *
 * @author Clinton Begin
 */
public class InterceptorChain {

    /**
     * 拦截器数组
     */
    private final List<Interceptor> 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<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

}

InterceptorChain内部维护了一个Interceptor列表,用来存放所有自定义的插件。

插件如何生效

注意到上面自定义的插件类上有如下注解

@Intercepts(@Signature(type = ResultSetHandler.class,//指定你要拦截是哪个对象
        method = "handleResultSets",//指定你要拦截的是哪个方法
        args = {Statement.class}))//考虑到方法重载,还需要指定参数列表

该注解声明了该插件生效的时机:mybatis在调用ResultSetHandler#handleResultSets(Statement stmt)方法时会执行插件逻辑。

实际上ResultSetHandler是一个接口,它的默认实现类是DefaultResultSetHandler

我们知道,mybatis最终是通过ResultSetHandler来处理从数据库查询的结果的,

那就来找到创建ResultSetHandler的地方

// 创建 ResultSetHandler 对象
    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                                ResultHandler resultHandler, BoundSql boundSql) {
        // 1、创建 DefaultResultSetHandler 对象
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        // 2、应用插件
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }

重点关注步骤2,在步骤1创建了一个DefaultResultSetHandler创建了一个默认的ResultSetHandler实现以后,调用了InterceptorChain#pluginAll方法,继续跟踪该方法,最终发现是遍历InterceptorChain保存的所有的插件,然后逐个调用插件的plugin方法

/**
     * 应用所有插件
     *
     * @param target 目标对象
     * @return 应用结果
     */
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

紧接着再来看看Interceptor#plugin方法

Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

内部调用了Plugin类的静态wrap方法

高能来了!高能来了!高能来了!

/**
     * 创建目标类的代理对象
     *
     * @param target 目标类
     * @param interceptor 拦截器对象
     * @return 代理对象
     */
    public static Object wrap(Object target, Interceptor interceptor) {
        // 获得拦截的方法映射,哪个类的哪些方法需要被拦截
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 获得目标类的类型
        Class<?> type = target.getClass();
        // 获得目标类的接口集合
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        // 若有接口,则创建目标对象的 JDK Proxy 对象
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
        }
        // 如果没有,则返回原始的目标对象
        return target;
    }

Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));

这行代码难道你不熟悉吗,没错,这正是JDK动态代理的写法。

从这里可以看出来mybatis创建的ResultSetHandler对象其实是经过层层代理过的对象。

那既然是JDK动态代理,那就应该有对InvocationHandler的实现,是谁呢?

看看Plugin这个类的定义public class Plugin implements InvocationHandler会发现就是它了。

紧接着来看看InvocationHandler最重要的invoke方法的实现

@Override
    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)) {
                // 1、如果当前方法包含在要被拦截的方法之内,则拦截处理该方法
                return interceptor.intercept(new Invocation(target, method, args));
            }
            // 2、如果不是,则调用原方法
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }
  1. 如果当前方法要被拦截,那就调用插件的intercept方法;
  2. 如果当前方法不需要被拦截,那就直接调用原始对象的对应方法;

看到这里就豁然开朗了,原来自定义插件时实现的intercept方法是在这里被调用了。

整体思路

现在来整理一下思路:

1、将自定义插件保存到插件链中;
在这里插入图片描述
2、使用插件链中的插件层层代理ResultSetHandler
在这里插入图片描述
3、调用插件的intercept方法;
在这里插入图片描述

总结

经过上面的分析,发现Mybatis的插件机制主要还是依赖于JDK动态代理,更抽象一点说是依赖于AOP思想,其核心就是“拦截+增强”,那其实除了Mybatis的插件机制,Skywalking的插件机制也是基于这种思想实现的,可以参考:Skywalking如何通过修改字节码让插件生效

工作中我们也可以尝试借鉴这种思想来扩展我们的业务服务。

最后让我们一起喊出:动态代理,yyds!


相关代码请参考:https://github.com/xujian01/blogcode/tree/master/src/main/java/mybatisplugin

posted @ 2021-05-19 15:30  墨、鱼的blog  阅读(546)  评论(0编辑  收藏  举报