Mybatis的插件机制解析
Mybatis提供了插件机制,让我们有机会拦截其执行过程,插入我们自定义的逻辑,比如分页、SQL执行性能监控、数据库表公共字段赋值等。
学习Mybaits的插件原理,最好的方式是首先来写一个简单的插件,这个插件没什么作用,就是往控制台输出点日志。
非常简单的LogPlugin插件
Mybatis的插件都必须实现Interceptor接口,它定义了3个方法如下:
public interface Interceptor {
//拦截方法,拦截逻辑写在这里面
Object intercept(Invocation invocation) throws Throwable;
//包装目标对象方法,通过代理返回包装后的代理对象
Object plugin(Object target);
//插件初始化,调用该方法设置配置的属性。
void setProperties(Properties properties);
}
LogPluginV1的实现,可以看到它只实现了plugin方法,没有实现intercept方法:
public class LogPluginV1 implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
@Override
public Object plugin(final Object target) {
//只拦截Executor类型
if(Executor.class.isAssignableFrom(target.getClass())){
System.out.println("----调用LogPlugin.plugin方法,对目标对象["+target+"]进行包装---");
//返回target的动态代理对象
return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Executor.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + method.getName()+"执行前打印---");
Object result = method.invoke(target, args);
System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + method.getName()+"执行后打印---");
return result;
}
});
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
}
将LogPluginV1插件配置到Mybatis的主配置文件中:
<plugins>
<plugin interceptor="com.zyy.demo.plugin.LogPluginV1"/>
</plugins>
跑下我们的测试用例(简单的SelectById查询),可以看到类似的输出:
----调用LogPlugin.plugin方法,对目标对象[org.apache.ibatis.executor.CachingExecutor@5f3a4b84]进行包装---
----调用目标方法:CachingExecutor.query执行前打印---
....
----调用目标方法:CachingExecutor.query执行后打印---
----调用目标方法:CachingExecutor.close执行前打印---
----调用目标方法:CachingExecutor.close执行后打印---
可以看出,我们的插件以及其作用了,它对CachingExecutor@5f3a4b84
这个对象进行包装,并拦截了其中的query和close方法,执行了我们的输出逻辑。
那么Mybatis是如何做到的呢?
Mybatis插件原理
首先,Mybatis要让我们自定义的插件生效,第一步就是将自定义的插件配置解析出来,在之前的文章 Mybatis初始化源码解析(https://blog.csdn.net/gruelxsp/article/details/103803744) 中,详细的阐述了Mybatis的配置解析,这儿简单的回顾下对插件的解析,XMLConfigBuilder#pluginElement方法:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//遍历plugins标签下配置的所有plugin
for (XNode child : parent.getChildren()) {
//获取interceptor属性,即插件类的全路径
String interceptor = child.getStringAttribute("interceptor");
//获取对应的配置属性
Properties properties = child.getChildrenAsProperties();
//通过反射实例化插件对象,注意是通过调用默认的无参构造方法来实例化的,因此如果插件没有无参构造方法,会抛出实例化错误。
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//调用自定义插件的setProperties方法,来设置自定义插件的属性。(当然你可以在里面实现额外的逻辑)
interceptorInstance.setProperties(properties);
//将创建好的创建实例报错到configuration的interceptorChain中。
configuration.addInterceptor(interceptorInstance);
}
}
}
接下来,需要将插件应用到我们想要拦截的对象上去,才能执行到我们插件的代码。Mybatis会在4个地方进行插件应用,Mybatis的详细的执行流程解析 见 文章 https://blog.csdn.net/gruelxsp/article/details/103813154,这儿就直接给出4个插件应用点:
第一个是Configuration#newExecutor方法:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
//按插件加载顺序应用上所有插件,先配置的先应用,后配置的后应用。
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
第二个是Configuration#newStatementHandler方法:
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;
}
第三个是Configuration#newParameterHandler方法:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//应用插件
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
第四个是Configuration#newResultSetHandler方法:
ublic 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;
}
而interceptorChain.pluginAll方法非常简单,就是依次调用插件的plugin方法:
public Object pluginAll(Object target) {
//依次调用插件的plugin方法,对目标对象完成一层一层的包装(就像洋葱一样,如果你愿意一层一层的剥开我的心....)
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
现在回过头看下LogPluginV1的plugin方法,它会在上面提到4处都会执行一次,但只有在第一处,目标对象是Executor类型时,才进行了动态代理,其他三处没做包装,返回的就是原对象,因此我们也只有调用到Executor的方法时才会被拦截,执行我们的日志输出。至于怎么调用到 的方法的(比如示例中的query和close)请参考 Mybatis的详细的执行流程解析(https://blog.csdn.net/gruelxsp/article/details/103813154)
Mybatis的Plugin工具
也许你会感到有点奇怪,在Interceptor接口中定义了Object intercept(Invocation invocation)
方法,而我们的LogPluginV1插件根本没有实现它,而且从前面的源码分析中,也根本没有看到这个方法的身影,难道它是一个无用的方法?
为了说清楚这个方法的作用,我们来实现LogPluginV2版本的插件,作用和V1版本的一样,只不过实现方式略有不同:
//通过@Intercepts注解来指明,要拦截的是哪种类型的哪个方法。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class LogPluginV2 implements Interceptor {
//实现拦截逻辑
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + invocation.getMethod().getName()+"执行前打印---");
Object result = invocation.proceed();
System.out.println("----调用目标方法:"+target.getClass().getSimpleName() + "." + invocation.getMethod().getName()+"执行后打印---");
return result;
}
@Override
public Object plugin(final Object target) {
//通过Mybatis提供的Plugin工具类来完成目标对象的包装。
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
相比于V1,V2版本使用Plugin工具类来辅助我们完成插件的开发,将包装目标对象的代码和自定义逻辑的代码分离,这样让代码更清爽,这儿就将intercept方法给用上了。
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.wrap(target, this);`
public static Object wrap(Object target, Interceptor interceptor) {
//根据自定义插件的@Intercepts注解,解析出要拦截的类型以及对应的方法。
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//获取目标对象的所有会代理的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//如果有需要代理的接口,返回动态代理对象,对目标对象进行包装。注意Plugin本身实现了InvocationHandler接口。
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 {
//当前类型声明要拦截的方法集合,如果当前方法在这个集合中,则调用插件的intercept方法
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注解对象
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
//使用Plugin.wrap方法进行包装时,必须要有@Intercepts注解
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>>();
//遍历注解的全部签名,构建出key为类型、value为Method集合的map结构。
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//注意,注解是类型class的getMethod方法,传入方法名和参数类型,必须要获取到对应的方法,否则会抛出异常
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<Class<?>>();
//遍历当前类以及其所有祖先类。
while (type != null) {
//当前类的所有接口,如果在注解中申明要拦截,这加入list。
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
可以看出有了Plugin工具,插件的开发非常简单了,而且可以注解声明的方式来指定想拦截什么方法就拦截什么方法,而不是像V1版本哪样写在代码逻辑里面。
插件总结
最后,简单的总结下,Mybatis的插件原理其实非常简单,就是对Mybatis执行流程中用到的4个核心类型(Executor,StatementHander、ParameterHandler、ResultSetHandler)生成代理对象,从而拦截4个核心类型的方法,实现自定义逻辑。
Mybatis还非常贴心的为我们提供了Plugin工具,通过注解的方式指定要拦截的类型和方法签名,进一步简化插件的开发,非常的轻巧,方便和实用。