前言
Mybatis的插件,是Mybatis提供的一种拓展机制,它可以针对Mybatis中的四大组件进行功能增强,这四大组件分别是:Executor(执行器)、StatementHandler(语句处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果集处理器)。
Mybatis插件基于JDK动态代理,应用了责任链模式,实现了对源代码的零侵入增强。同时,还支持开发者自定义插件,实现自己想要的功能。
今天就来分享一下Mybatis中的插件机制,包括自定义插件实现、插件机制源码解析(初始化流程、调用拦截机制)等,希望可以帮到大家,谢谢~
自定义插件
俗话说,先会走才能跑,学习框架也是同理。一个功能,得先会用,再去学习其源码及优秀设计思想。
这里就先来实现一个自定义插件,具体功能就是打印执行的sql语句,这个小案例旨在展示自定义插件的使用方法。
先介绍一下需要用到的API:两个注解 + 一个接口
@Intercepts
这个注解,是用来标在自定义插件类上面的,功能是用来定义当前插件需要增强的所有组件的方法。该注解有一个属性,是一个Signature数组,这个数组中的每一个元素,就是一个可以被拦截的组件方法。
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
@Signature
这个注解,就是用来具体的定义一个可以被增强的组件的方法,里面有三个属性:
- Class<?> type():用来指定当前要增强哪个组件,例如语句处理器:StatementHandler.class
- String method():用来指定要增强组件中的哪个方法,例如:prepare()方法
- Class<?>[] args():要增强方法的参数类型,因为存在重载方法,所以要指定参数类型,才能确定唯一方法。
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
Interceptor接口
这个接口是插件实现的根本,每个插件都要实现这个接口,重写里面的方法。
java
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:插件拦截组件之后,需要执行的增强逻辑。入参invocation为目标对象。
- plugin:生成目标对象(待增强组件)的代理对象。
- setProperties:在插件初始化时,设置一些属性值。通过调用Properties的get或setProperty方法,设置或获取值。
plugin()方法中,又调用了Plugin.wrap(target, this)来生产代理对象,至于为什么这样就可以生产代理对象,我们在后面的源码解析中进行说明,这里大家就知道,这个方法可以生产代理对象即可。
案例
现在我想指定一个自定义插件MyPlugin,在执行sql语句时将sql语句进行打印。想实现这个自定义插件,需要三步
- 创建一个自定义插件类,实现
Interceptor接口,重写里面的方法,添加自己的增强逻辑。 - 自定义插件类上面标注
@Intercepts、@Signature注解,来指定当前插件要增强哪些组件的哪些方法。 - 将自定义插件进行配置,例如在xml核心配置文件中配置
\<plugins>标签。
下面这个案例,就是去增强了StatementHandler组件的prepare()方法,在执行的时候,打印当前封装好的sql语句,大家可以做个参考。
java
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
String sql = handler.getBoundSql().getSql();
System.out.println("【sql】:" + sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:" + properties);
}
}
xml
<configuration>
<!--注册插件-->
<plugins>
<plugin interceptor="com.sxb.interceptor.MyPlugin"></plugin>
</plugins>
<!--省略其它配置项...-->
</configuration>
源码解析
核心配置文件解析
Mybatis中对于配置文件的解析,是通过SqlSessionFactoryBuilder类的build方法来完成的,过程中解析了配置文件,创建了SqlSessionFactory工厂对象。解析过程中,经历了XMLConfigBuilder解析核心配置文件、XMLMapperBuilder解析映射配置文件等等。
所以对于核心配置文件中<plugins>标签的解析,肯定是在build方法中完成的。在build方法中,创建了XMLConfigBuilder对象,调用它的parse()方法来解析配置文件,过程中,对于configuration根标签下的每一个子标签都进行了解析,例如<mappers>、<plugins>、<environments>等等。
下面是XMLConfigBuilder中的一个重要方法parseConfiguration(),对于每一个子标签都进行了解析。
java
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
重点看一下第9行pluginElement(),这个方法就是对<plugins>标签进行了解析,看一下源码:
java
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();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
- 第2-5行:方法一开始进行了判空,然后遍历,取出每一个
<plugin>标签的属性interceptor的值(这里是配置的自定义插件的全路径),接下来获取了 <plugin> 标签内部的<property>子标签,封装为了Properties对象。 - 第6行:通过反射的方式,拿到当前插件的构造器对象,然后调用
newInstance方法创建了实例。 - 第7行:调用了插件实例的
setProperties方法,将上一步取出的 Properties 对象进行了传入。这里实际就是调用了实现Interceptor接口时重写的方法中。这里我们可以明白:Properties对象是在这里进行了封装,值就是获取的<plugin>内部的所有<property>子标签内容。 - 第8行:调用全局配置对象
configuration的addInterceptor方法,将当前插件,添加到了插件拦截器链中。看一下这个方法的源码:
java
public class Configuration {
//省略其它属性和方法...
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
}
可以看到,插件的添加操作,被继续委派给了Configuration类中的一个成员变量interceptorChain,顾名思义就是拦截链,这里调用了拦截链对象的addInterceptor方法。看一下 InterceptorChain 这个类的结构:
java
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;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
- 首先可以看到,在这个类中,维护了一个List集合
interceptors,泛型是Interceptor,不难理解,这个集合中就维护了所有的插件。 addInterceptor方法和getInterceptors方法,就是对这个插件集合的操作:添加插件和获取插件。- pluginAll 方法比较重要,我们来单独介绍一下
pluginAll方法
我们先来看一下,这个方法是在什么位置调用的
这个方法的调用位置,总共有四处,都在全局配置类Configuration中:
java
public class Configuration {
//省略其它属性和方法...
protected final InterceptorChain interceptorChain = new InterceptorChain();
//参数解析器构建方法
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//结果集处理器构建方法
public 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;
}
//语句处理器构建方法
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;
}
//执行器构建方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
从上面的代码我们可以看出:这四个方法,不就是文章开头提到的 可以被插件增强的四大组件 的构建方法吗?
在构建的过程中,每个组件都会调用拦截链interceptorChain的pluginAll方法,传入当前构建好的对象,这里获取的返回值,如果配置了插件,返回的就是代理对象,反之,就是原对象。为什么这么说呢?还是得看一下pluginAll方法的逻辑
java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
方法的逻辑比较简单:就是去遍历了拦截链,然后,调用了每一个插件的plugin方法,还记得这个方法的逻辑吗?这个方法是default修饰的,在接口中就将逻辑定义好的方法。
java
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
可以看到,plugin方法中,调用了Plugin这个类的静态方法wrap,传入的参数中,target就是上一步调用时,传入的四大组件的实例,还传入了this,就是当前自定义插件的实例。那为什么调用了这个方法,就能获取到组件实例的代理对象呢?这得看一下Plugin这个类的结构:
java
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final 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;
}
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;
}
@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);
}
}
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<>();
for (Signature sig : sigs) {
Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, 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;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[0]);
}
}
-
看到这里,就很明朗了,因为Plugin这个类,实现了
InvocationHandler接口,这是一个函数式接口,是搭配JDK动态代理来使用的,重写了接口中的invoke方法后,使用代理对象调用目标方法时,就会首先调用到invoke方法,实现横向的逻辑增强。可以用一张图来加强一下理解:
类中也的确重写了
invoke方法,方法中,有一行比较重要的代码:interceptor.intercept(new Invocation(target, method, args)),这里就是去调用了插件的intercept方法,也就是 自定义的拦截逻辑。看到这里,首先就可以明白:Plugin类,实现了
InvocationHandler接口,重写了invoke方法,那么在创建了四大组件的代理对象之后,代理对象调用目标方法,都会转到invoke方法中,而这个方法中,又调用了插件对象的intercept方法,就是通过这样的方式,完成了插件拦截逻辑。 -
wrap方法中的逻辑也很简单,就是获取到当前插件可以增强的方法之后,调用了
Proxy对象的newProxyInstance方法,创建了代理对象,调用时传入的InvocationHandler实例,就是去创建了一个Plugin对象。- 这个方法中,还进行了
@Intercepts @Signature注解的扫描封装,调用了getSignatureMap方法,这个方法中获取了插件类上面标记的注解,获取了配置的属性。最后将所有可以增强的方法,封装成了一个Set<Method>集合,最后在调用intercept增强逻辑之前,进行了判断,当前方法是否存在于set集合,是 则调用增强逻辑,否 则不进行增强。
- 这个方法中,还进行了
原来,我们所说的插件可以增强四大组件,原因就在于:这四大组件的构建过程中,都去尝试获取了代理对象。
到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~