好的框架在设计的时候肯定会预留一些扩展点方便使用者去做一些可插拔式的定制需求开发,MyBatis 插件机制就是为方便扩展而设计的,这篇文章就来简单介绍一下。
MyBatis 插件概述
MyBatis 插件虽然配置的时候是以 <plugin>
标签的形式声明的,但实际上它就是一个拦截器,应用代理模式在创建特定类的时候生成代理对象,在方法级别上进行拦截。例如前面介绍的 Executor
StatementHandler
ParameterHandler
和 ResultSetHandler
。
实现原理
全局唯一配置类 Configuration
中持有一个 InterceptorChain
类型的 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;
}
//将配置文件中的 <plugin> 加入到拦截器链中
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
//查询所有的拦截器链
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
可以看出最重要的逻辑就在生成代理对象这一步,大多数情况下都是使用 org.apache.ibatis.plugin.Plugin#wrap
来完成代理对象的创建,一起来看看具体逻辑
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;
}
方法①重要是解析拦截器上标注的
@Intercepts
@Signature
这两个注解,获取拦截器的配置元信息。
方法②利用 JDK 动态代理生成代理对象。
@Intercepts 和 @Signature
这两个注解用于自定的拦截器上,用来说明当前拦截器需要对哪些类、哪些方法进行拦截处理。
- Class<?> type :需要拦截目标对象的类。
- String method:需要拦截目标类的方法名。
- Class<?>[] args:需要拦截目标类的方法名的参数类型签名。
代理对象的执行
上面通过 JDK 动态代理创建代理对象的第三个参数是 Plug
类,该类实现了 InvocationHandler
,所以代理对象的执行会调用其重写的 invoke
方法,代码如下:
//org.apache.ibatis.plugin.Plugin
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)) {
//如果需被拦截的方法集合包含当前执行的方法,则执行拦截器的interceptor方法
return interceptor.intercept(new Invocation(target, method, args));
}
//如果不是,则直接调用目标方法的Invoke方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
从该方法可以看出 Interceptor
接口的 intercept
方法就是拦截器自身需要实现的逻辑,其参数为 Invocation
,在该方法的结束,需要调用 invocation#proceed()
方法,进行拦截器链的传播。
实践
- 创建自定义的拦截器,必须实现实现
Interceptor
接口 - 实现
plugin
方法,在该方法中决定是否需要创建代理对象,如果需要,直接使用Plugin#wrap
方法创建 - 实现
interceptor
方法,该方法中定义拦截器的逻辑,并且在最后请调用invocation.proceed()
方法传递拦截器链 - 在拦截器上标注
@Intercepts
@Signature
指定需要拦截的类和方法 - 在配置文件
plugin
标签中配置该拦截器
下面举个多租户场景下查询时自动添加租户逻辑的拦截器示例:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class Wyc1856Interceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (invocation.getTarget() instanceof RoutingStatementHandler) {
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
//反射获取需要的属性
BaseStatementHandler baseStatementHandler = (BaseStatementHandler) ReflectHelper.getFieldValue(statementHandler, "delegate");
MappedStatement mappedStatement = (MappedStatement) ReflectHelper.getFieldValue(baseStatementHandler, "mappedStatement");
//判断是查询语句
if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)){
//取原始 SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
//拼装租户条件
sql = sql + " and `tenant_id` = " + AppContext.getTenantId();
//反射重新设置 sql
ReflectHelper.setFieldValue(boundSql, "sql", sql);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler){
//创建代理对象
target = Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {}
}
使用场景
MyBatis-PageHelper
分页功能- 公共字段统一赋值
- SQL 性能监控
- 其他需要在 SQL 执行的各个阶段做扩展