前言
又来更新了,上半年就准备写这个知识点了,因为参与一个JAM活动而耽搁了一段的时间,久到忘记这个系列的存在。剩下的几个章节里,我会详细讲一下插件的写法,然后展开介绍一下我之前写过的两个插件。
插件是是什么
在mybatis的框架里,允许开发者对sql的执行过程,进行拦截增强处理,用来实现特殊业务逻辑。
插件接口
所有的自定义插件,都需要实现此接口,可以说,这就是插件的入口了。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
这个接口中的三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:
- intercept:这个就是具体的拦截方法,在自定义插件中,在这个方法中,实现需要的业务逻辑。
- plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般只有四个类型:Executor,ParameterHandler,ResultSetHandler,StatementHandler。
- setProperties:这个方法用来传递插件的参数,在XML中,通过对指定插件属性设置,用以执行特定逻辑分支。
增强逻辑intercept
一般自定义插件只需要实现此接口,从Interceptor接口的定义可以看出端倪,其他两个方法都有默认实现,定义了default关键字。
根据拦截方法不同,可以从Invocation对象中获取原始参数
- getTarget():获取被代理对象;
- getMethod():获取被代理的方法;
- getArgs():获取方法参数值;
- proceed():执行方法,直接反射执行:method.invoke(target, args);
以分页插件的部分代码为例:
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
// ...
}
核心方法plugin
从上面的方法介绍,就可以看出来,plugin方法扮演了非常重要的角色,参数里的target,是需要被代理的对象。在这个方法里,会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过JDK动态代理的方式,拦截并增强目标对象。
public static Object wrap(Object target, Interceptor interceptor) {
// 自定义插件里,指定的接口和方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 被代理对象类
Class<?> type = target.getClass();
// 从@Signature注解中,获取需要被代理的接口,逻辑是:如果注解中指定的类,是包含被代理对象,或者包含被代理对象的父类
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 返回动态代理对象,最终执行的不是原始的方法,而是此动态代理的方法
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
注意一:
在getSignatureMap()方法里,通过@Signature注解里定义的方法名和参数,来获取唯一的方法。这就需要我们在定义注解的时候,注意方法名、参数类型和参数个数,才能找到需要被代理的方法。
Method method = signature.type().getMethod(signature.method(), signature.args())
注意二:
如果我们定义一个插件,指定其他类型,比如Object,会怎样呢?根据if条件来看的话,只会返回原始类型,不会进行任何增强处理。
if (interfaces.length > 0)
没有存在感的setProperties
在我后续的例子中,基本没有用到此特性,小透明实锤了。
<plugins>
<plugin interceptor="XXX.XXX.plugin.XXXInterceptor">
<property name="xxx" value="xxx"/>
</plugin>
</plugins>
在XMLConfigBuilder类中,读取此配置:
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).getDeclaredConstructor().newInstance();
// 加载xml中定义的属性,赋予指定插件
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
相关注解
涉及到两个注解
- @Intercepts:指定要拦截的目标方法;
- @Signature:指定需要拦截的方法签名;
拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:
- type: 拦截器需要拦截的接口,只能是下面列出的四个,至于为什么,看后续说明;
- method: 拦截器所拦截接口中的方法,与type指定的接口匹配;
- args: 拦截器所拦截方法的参数类型;
允许拦截的接口
- Executor:执行器,提供操作数据库的接口;
- ParameterHandler:参数处理器,设置sql的参数;
- ResultSetHandler:结果集处理器,处理从数据库查询的结果集,封装成对象等;
- StatementHandler:语法处理器,真正去执行数据库CRUD。
为什么是这四个接口
依我看来,用这四个接口可以完全诠释一个sql执行的生命周期,从语法定义到执行,再到结果生成。所以我们需要自定义的插件,全程围绕这四个接口开展。
InterceptorChain
再来看个插件的核心类,链式插件集合。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
// 匹配目标对象的插件
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 启动服务的时候,将自定义的插件加载到mybatis里的上下文环境
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
有人就会说了,看了这个类,也不理解为什么接口需要限定那四个啊!看下面这个截图,引用pluginAll方法的地方,指定了返回类型是Executor,ParameterHandler,ResultSetHandler,StatementHandler。
插件执行图例
假设我们在StatementHandler接口上定义了三个插件,那么selectXxx的执行逻辑如下: