MyBatis插件平台原理详解
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/72/ )。
在前面的文章中我们已经讲过,MyBatis支持插件开发,并且实际开发了一个插件示例。本文将介绍MyBatis的插件原理。
该文的内容均参考自《通用源码阅读指导书——MyBatis源码详解》一书,要想深入了解,则可以阅读该书籍。
MyBatis在plugin包中搭建了一个拦截器平台。下图给出了拦截器平台的类图。
整个类图中最为核心的类便是Plugin类,它继承了java.lang.reflect.InvocationHandler接口,因此是一个基于反射实现的代理类。Plugin类的属性如下所示。
// 被代理对象
private final Object target;
// 拦截器
private final Interceptor interceptor;
// 拦截器要拦截的所有的类,以及类中的方法
private final Map<Class<?>, Set<Method>> signatureMap;
Plugin类的signatureMap属性存储的是当前拦截器要拦截的类和方法,该信息就是通过getSignatureMap方法从拦截器的Intercepts注解和Signature注解中获取的。下面给出了getSignatureMap方法的源码。
/**
* 获取拦截器要拦截的所有类和类中的方法
* @param interceptor 拦截器
* @return 入参拦截器要拦截的所有类和类中的方法
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 获取拦截器的Intercepts注解
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());
}
// 将Intercepts注解的value信息取出来,是一个Signature数组
Signature[] sigs = interceptsAnnotation.value();
// 将Signature数组数组放入一个Map中。键为Signature注解的type类型,值为该类型下的方法集合
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
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会为这个类创建一个代理类。这部分操作在wrap方法中完成,下面给出了wrap方法的源码。
/**
* 根据拦截器的配置来生成一个对象用来替换被代理对象
* @param target 被代理对象
* @param interceptor 拦截器
* @return 用来替换被代理对象的对象
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 得到拦截器interceptor要拦截的类型与方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 被代理对象的类型
Class<?> type = target.getClass();
// 逐级寻找被代理对象类型的父类,将父类中需要被拦截的全部找出
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 只要父类中有一个需要拦截,说明被代理对象是需要拦截的
if (interfaces.length > 0) {
// 创建并返回一个代理对象,是Plugin类的实例
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 直接返回原有被代理对象,这意味着被代理对象的方法不需要被拦截
return target;
}
因此,如果一个目标类需要被某个拦截器拦截的话,那么这个类的对象已经在warp方法中被替换成了代理对象,即Plugin对象。当目标类的方法被触发时,会直接进入Plugin对象的invoke方法。在invoke方法中,会进行方法层面的进一步判断:如果拦截器声明了要拦截此方法,则将此方法交给拦截器执行;如果拦截器未声明拦截此方法,则将此方法交给被代理对象完成。invoke方法的源码如下所示。
/**
* 代理方法
* @param proxy 代理对象
* @param method 代理方法
* @param args 代理方法的参数
* @return 方法执行结果
* @throws Throwable
*/
@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)) {
// 该方法确实需要被拦截器拦截,因此交给拦截器处理
return interceptor.intercept(new Invocation(target, method, args));
}
// 这说明该方法不需要拦截,交给被代理对象处理
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
所以Plugin类完成了类层级和方法层级这两个层级的过滤工作:
- 如果目标对象所属的类被拦截器声明拦截,则Plugin用自身实例作为代理对象替换目标对象。
- 如果目标对象被调用的方法被拦截器声明拦截,则Plugin将该方法交给拦截器处理。否则Plugin将该方法交给目标对象处理。
正因为Plugin类完成了大量的工作,拦截器自身所需要做的工作就非常简单,主要分为两块:使用Intercepts注解和Signature注解声明自身要拦截的类型与方法;通过intercept方法处理被拦截的方法。
当然,拦截器也可以重写Interceptor接口中的plugin方法,来实现更为强大的功能。
重写plugin方法后,可以在plugin方法中给出一个其他的类来替换目标对象(而不调用Plugin类的warp方法)。这样可以完全脱离Plugin类去完成一些更为自由的操作。这种情况下,如何替换目标对象以及替换之后的处理逻辑完全由插件开发者自己掌控。
现在我们了解到拦截器的生效原理,那MyBatis支持接入配置多个拦截器么?
答案是肯定的。我们可以在MyBatis的配置文件中配置多个插件,这些插件会在MyBatis的初始化阶段被依次写入到InterceptorChain类的interceptors列表中。这一过程在XMLConfigBuilder类的pluginElement方法中开展,如下所示。
/**
* 解析<plugins>节点
* @param parent <plugins>节点
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) { // <plugins>节点存在
for (XNode child : parent.getChildren()) { // 依次取出<plugins>节点下的每个<plugin>节点
// 读取拦截器类名
String interceptor = child.getStringAttribute("interceptor");
// 读取拦截器属性
Properties properties = child.getChildrenAsProperties();
// 实例化拦截器类
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// 设置拦截器的属性
interceptorInstance.setProperties(properties);
// 将当前拦截器加入到拦截器链中
configuration.addInterceptor(interceptorInstance);
}
}
}
这些拦截器在列表中组成了一个拦截器链。
我们还了解到拦截器是通过替换目标对象实现的(通常基于Plugin类,使用动态代理对象替换目标对象),那MyBatis中任何的对象都可以被替换么?
答案是否定的。MyBatis中一共只有四个类的对象可以被拦截器替换,这四个类分别是ParameterHandler、ResultSetHandler、StatementHandler、Executor。而且替换只能发生在固定的地方,我们称之为拦截点。以ParameterHandler对象为例,下面代码给出了ParameterHandler对象的拦截点。
/**
* 创建参数处理器
* @param mappedStatement 数据库操作的信息
* @param parameterObject 参数对象
* @param boundSql SQL语句信息
* @return 参数处理器
*/
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 创建参数处理器
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 将参数处理器交给拦截器链进行替换,以便拦截器链中的拦截器能注入行为
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
// 返回最终的参数处理器
return parameterHandler;
}
在ParameterHandler对象的拦截点,ParameterHandler对象被作为参数传递给拦截器链的pluginAll方法,以便于拦截器链中的拦截器能够将行为注入到ParameterHandler对象中。InterceptorChain类的pluginAll方法如下所示。
/**
* 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
* @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
* @return 用来替换目标对象的对象
*/
public Object pluginAll(Object target) {
// 依次交给每个拦截器完成目标对象的替换工作
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
在InterceptorChain类的pluginAll方法中,会将目标对象依次交给每个拦截器进行替换处理(通常是对目标对象进行进一步的包装以注入拦截器的功能),最后得到的目标对象target汇聚了拦截器链中的每一个拦截器的功能,这其实就是责任链模式。这样,在程序运行中,拦截器链中的各个拦截器会依次发挥自身的作用。
以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。推荐给大家。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。
