在这篇文章我将介绍插件的源码,MyBatis插件是个很好的功能,类似于Spring的Aop,使得我们使用者可以较为灵活的在流程的特定点对SQL进行操作。
允许我引用网上的一段介绍,因为觉得总结得很好:MyBatis 允许我们以插件的形式对已映射语句执行过程中的某一点
进行拦截调用
,通俗一点来说,MyBatis 的插件其实更应该被称作为拦截器,总体上来说插件很好很好理解,因此这篇文章不会很长,所以为了扩充一下文章长度,最后将用Mybatis-Plus 的分页插件作为例子,讲解一下这个是如何切入的。
1. 基本使用
一般是这么使用:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
@Component
public class TestExecutorInterceptor implements Interceptor{
}
MyBatis 允许插件拦截如下 4 个对象的方法。
- Executor的 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 方法
- ParameterHandler的 getParameterObject, setParameters 方法
- ResultSetHandler的 handleResultSets, handleOutputParameters 方法
- StatementHandler的 prepare, parameterize, batch, update, query 方法
如果想知道为啥是这几个,可以点开InterceptorChain的pluginAll方法,就可以发现是以上几个类调用了。我们可以发现InterceptorChain,里面有个interceptors维护了Interceptor的实现类list,因此当我们需要运用插件功能的时候,就需要新建一个Interceptor实现类并实现intercept方法,再将它声明成一个Bean,使得它可以添加进interceptors中,再在以上说的4个类相关的方法被调用时,生成代理类并调用。以上大致插件的基本使用和原理,那么让我们开始吧。
2. 从Executor开始
根据我们上面说的方法,我就拿Executor来举例了,那么点进newExecutor方法,也是我上篇涉及过方法
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 = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
让我们看看pluginAll。
public Object pluginAll(Object target) {
// 遍历interceptors,包含我们新建的。
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
pluginAll 很简单,只是遍历interceptors,并调用Interceptor的默认plugin方法,当然这只是一层皮,它又去调用Plugin的wrap方法。所以让我们看看Plugin的wrap方法。
public static Object wrap(Object target, Interceptor interceptor) {
// key 为Signature中的type,即class,value为Signature中的method集合
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获取target的类,这个的target是指我们要代理的类,而不是我们写的继承自Interceptor的类,这里指的是Executor
Class<?> type = target.getClass();
// 获取目标类所有实现的接口, 并且它在signatureMap key中。
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 通过Java的原生代理为目标类生成代理类。
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
2.1 获取Intercepts注解等信息
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());
}
// 获取注解上的 Signature注解数组
Signature[] sigs = interceptsAnnotation.value();
// Signature注解的上 类和方法的map
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;
}
这个方法很好理解,就是获取signatureMap这个map。
再看下getAllInterfaces
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
// 遍历代理类实现的所有接口,并遍历,如果signatureMap中包含接口名,那么添加至interfaces里面。
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
这个方法也不难理解,至于为啥要使用这个方法,就拿我们用Executor来说,在newExecutor里也看见了,我们参数传递的是CachingExecutor,也就是Executor的实现类,但我们在最开始的TestExecutorInterceptor里声明的是Executor,所以这样就能避免本应该被代理的CachingExecutor没被代理到。
2.2 代理方法
Plugin实现了 InvocationHandler 接口,因此会调用 invoke 方法拦截被代理类所有方法的调用,那么看下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)) {
// 是的话,就执行插件的逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
// 不是的话直接走原来的逻辑。
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke代码比较少,主要是判断拦截的方法是不是我们声明要切入的方法,是的话就会走我们自定义类下的intercept方法了。
此外,可以注意到实现Interceptor的类不止一个,InterceptorChain调用的也是pluginAll去遍历,所以最后生成的类其实是经过层层包裹过的,因此到时候就是从最外层开始慢慢向里调用。
3. 分析Mybatis-Plus分页插件源码
Mybatis-Plus是一款蛮好用的插件(此插件非彼插件),为啥不说很好用呢,因为我个人觉得他们在官网宣称的 只做增强不做改变这一点 有点意见,感觉并不像他们说的那样,而是有点强侵入的感觉了,当然这是我一家之言,而且它其他功能还是很方便的。
先看看官网的实例代码。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
官网的实例代码比较简单哈,就三行
- 新建一个MybatisPlusInterceptor
- 在其内部的list里加一个PaginationInnerInterceptor,这个类实现了InnerInterceptor
- 将这个MybatisPlusInterceptor 作为bean
@SuppressWarnings({"rawtypes"})
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
// Executor的query、update,StatementHandler的prepare进行拦截
public class MybatisPlusInterceptor implements Interceptor {
// 内部维护了一个InnerInterceptor list。
@Setter
private List<InnerInterceptor> interceptors = new ArrayList<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
// 如果目标类是Executor
if (target instanceof Executor) {
final Executor executor = (Executor) target;
// 获取方法参数里的第二个,就是所有的
Object parameter = args[1];
// 很明显Executor的update方法只有两个参数
boolean isUpdate = args.length == 2;
MappedStatement ms = (MappedStatement) args[0];
// 如果是查询
if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
BoundSql boundSql;
// 如果调用的是只有四个参数的查询方法,获取BoundSql
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
// 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
boundSql = (BoundSql) args[5];
}
// 遍历实现自InnerInterceptor的类
for (InnerInterceptor query : interceptors) {
// 判断是否能执行查询操作
// PaginationInnerInterceptor重写了它,进行count,如果count为0就返回false,不会去执行SQL了
// 虽然内部的逻辑还是去执行了SQL的,只是说
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
return Collections.emptyList();
}
// 执行查询钱的操作
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
} else if (isUpdate) {
// 如果是更新
for (InnerInterceptor update : interceptors) {
if (!update.willDoUpdate(executor, ms, parameter)) {
return -1;
}
update.beforeUpdate(executor, ms, parameter);
}
}
} else {
// StatementHandler
final StatementHandler sh = (StatementHandler) target;
Connection connections = (Connection) args[0];
Integer transactionTimeout = (Integer) args[1];
for (InnerInterceptor innerInterceptor : interceptors) {
innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
}
}
return invocation.proceed();
}
// 省略了无关的方法
}
该说的都在注释里了,为啥我说它强侵入呢,就是因为它把我们的查询条件的参数全部封装成了 一个参数,这样,想在不修改现有业务的情况下,想对特定类的传入参数进行改变,就会有很大限制,当然也不是完全无计可施,只是说更麻烦了。
以上!