Mybatis提供了插件机制,可以让我们介入到底层执行的一些流程,例如SQL执行前打印下SQL语句。这里的插件,在mybatis里面实际上是拦截器。在深入探究之前,先看看如何使用,然后再分析原理,最后会提一下笔者从里面得到的一些启发。
首先我们需要在mybatis的配置文件(mybatis-config.xml)里面配置好插件信息,如下所示:
<configuration>
<plugins>
<plugin interceptor="com.gameloft9.demo.dataaccess.interceptor.DialectStatementHandlerInterceptor">
<property name="debug" value="true"/>
plugin>
plugins>
configuration>
然后编写插件类,需要实现Interceptor接口:
/**
* SQL拦截器,控制日志打印
* @author gameloft9
* 2019-11-29
*/
@Slf4j
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class DialectStatementHandlerInterceptor implements Interceptor {
/**是否开启debug模式*/
private String debug;
// 拦截方法
public Object intercept(Invocation invocation) throws Throwable {
RoutingStatementHandler statement = (RoutingStatementHandler) invocation
.getTarget();
if ("true".equals(debug)) { // 打印日志
log.info("Executing SQL: {}", statement.getBoundSql().getSql().replaceAll("\\s+", " "));
log.info("\twith params: {}", statement.getBoundSql().getParameterObject());
}
// 拦截链继续执行
return invocation.proceed();
}
// 对拦截器进行代理,返回代理对象
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 可以读取xml的属性配置
public void setProperties(Properties properties) {
this.debug = (String) properties.getProperty("debug");
}
}
其中setProperties方法,可以将xml配置的属性注入到我们插件里面来。intercept就是具体的拦截方法,例如我们这里需要打印一下SQL语句,方便排查问题。plugin是一个包装动态代理的方法,为什么需要这个额外的方法?因为mybatis插件机制,底层是通过动态代理实现的。这个稍后会讲到。
现在我们完成了要做的事情(读取属性,打印SQL等),但是我们不知道它拦截的到底是哪个方法。这个功能需要两个注解来完成,@Intercepts和@Signature。@Intercepts用于接收Signature集合,@Signature则具体给出了需要拦截哪个类的哪个方法。例如上面我们要拦截的是StatementHandler.prepare(Connection)方法。
进行原理分析前,我们脑海里先问几个问题:
1-Interceptor是任何地方都可以拦截吗?
2-多个拦截器是如何一起工作的?
3-Plugin.wrap()这个别扭的方法是干什么的?
我们写插件,首先需要实现Interceptor这个接口:
public interface Interceptor {
// 拦截方法
Object intercept(Invocation var1) throws Throwable;
// 假装暂时不知道它是干嘛的
Object plugin(Object var1);
// 设置属性
void setProperties(Properties var1);
}
接口很简单,只有三个方法,里面出现了Invocation这个类,我们继续看:
public class Invocation {
private Object target;
private Method method;
private Object[] args;
//省略不相干的代码
......
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args); // 非常熟悉的反射调用
}
}
这个Invocation简单得不能再简单了,仅仅是对method的反射调用来了一层包装而已。看到这里我们隐隐约约感觉到事情有点不简单了,有经验的同学,看到这里就可能猜到可能和jdk的动态代理有什么关系。不过没猜到也没关系,我们从plugin方法开始入手。
在Interceptor里面的,我们实现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); // 拿到接口列表
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target; // jdk动态代理
}
看到了这里,有没有恍然大悟的感觉?Proxy.newProxyInstance,多么熟悉的代码,mybatis插件机制,是通过jdk动态代理实现的无疑了。
getSignatureMap通过拿到拦截器上面的@Intercept注解,把需要拦截的类及方法一个个找出来,存在了一个HashMap里面:
Map<Class<?>/**被拦截类*/, Set<Method>/**被拦截的方法*/> map。
getAllInterfaces首先拿到被代理对象的接口,如果刚好是需要拦截的类(signatureMap已经存了所有要拦截的类及方法),那么就添加到动态代理的接口列表里面。
至此,我们解决了问题3:Plugin.wrap这个别扭的方法是干什么的?它可以对目标类进行动态代理,将拦截器注入到InvocationHandler里面去,方便后面实现拦截逻辑。
很显然,重点就是这个plugin类,我们来看看它有什么:
/**
* Plugin实现了InvocationHandler,表明它就是动态代理的具体实现
*/
public class Plugin implements InvocationHandler {
private Object target; // 被代理的对象
private Interceptor interceptor; // 插件对象(本质上是一个拦截器),合适的时机就可以通过调用它处理拦截逻辑
private Map<Class<?>, Set<Method>> signatureMap; // 分析插件对象上的@Intercepts 注解后得到的需要拦截的接口、方法清单
// 动态代理
public static Object wrap(Object target, Interceptor interceptor) {
// 省略代码
}
// 具体实现
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass()); // 拿到所有需要拦截的方法
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args); // 如果刚好是需要拦截的,则进行拦截,否则放过去,调用原生方法
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
}
从上面的invoke代码中我们可以看到,当方法执行的时候,首先对方法进行了一个过滤操作,是我们要拦截的方法才调用拦截器的intercept方法,此时Invocation对象也出现了。这里就很自然的进入到我们的SQL插件的intercept方法里面来了:
// 拦截方法
public Object intercept(Invocation invocation) throws Throwable {
RoutingStatementHandler statement = (RoutingStatementHandler) invocation
.getTarget();
if ("true".equals(debug)) {
log.info("Executing SQL: {}", statement.getBoundSql().getSql().replaceAll("\\s+", " "));
log.info("\twith params: {}", statement.getBoundSql().getParameterObject());
}
// 拦截链继续执行
return invocation.proceed();
}
上面的拦截逻辑写完后,一定不要忘记加一句return invocation.proceed(),否则仅仅执行了这一个插件方法,后面的插件方法都被忽略了。
那么问题来了,我只看到了InvocationHandler里面存了一个interceptor,如果有多个interceptor,那么如何工作呢?
别急,我们发现了一个很有趣的类,InterceptorChain。顾名思义它应该就是组合多个拦截器的。
public class InterceptorChain {
// 拦截器列表
private final List<Interceptor> interceptors = new ArrayList();
// 把所有拦截器组合起来
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)i$.next();
}
return target;
}
// 省略不相干代码
}
说实话,当笔者看到pluginAll这个方法的时候,我感觉到了高潮。简单的循环里面,对target对象进行了层层代理!居然还可以这么玩?!通过这样一个层层代理,把所有的拦截器给串联了起来!至此,我们解决了问题2-多个拦截器是如何一起工作的?通过层层的jdk动态代理,把多个拦截器进行层层包装,最后形成一个大的动态代理对象。(要知道动态代理新生成的类,是放在元数据区,这样层层包装之后,最后元数据区是新增了一个类还是多个类呢?这个问题扯的有点远,暂且放下。)
下面我们搜搜看,看是哪里用到了这个pluginAll方法:
我们发现,ParamterHandler、Executor、StatementHandler、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); // 对statementHandler进行了层层代理
return statementHandler; // 返回了代理对象
}
以StatementHandler为例,我们创建完statementHandler对象后,对其进行了层层代理,然后返回了代理对象。这样执行方法的时候,很自然的会走到拦截链里面去。至此,我们解决了问题1-Interceptor是任何地方都可以拦截吗?不是的,只能够拦截ParamterHandler、Executor、StatementHandler、ResultSetHandler四个对象。
至此,Mybatis的插件机制就探究完了。但是笔者的思考并没有跟着结束,我看完后思考了两个问题:
在Interceptor接口中,定义了plugin方法:
Object plugin(Object target);
plugin方法主要是为了提供一个动态代理的包装,基本上插件的实现类都会这么写:
// 对拦截器进行代理,返回代理对象
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 基本就这就行了
}
如果仅仅是为了生成动态代理,而且每次都这么写一遍也挺无趣的。这个方法完全没有必要由插件类来实现。我们可以将其移到InterceptChain里面进行处理,例如:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 原来是target = interceptor.plugin(target);
target = Plugin.wrap(target,interceptor);
}
return target;
}
这样可以减少一个接口方法,插件类也不用关心如何去做动态代理了。那么这里为什么要把动态代理的过程暴露给插件实现类呢,我想可能作者有什么考量吧?在代理目标对象前,可能需要对其进行一些逻辑处理?
当我看到那个puginAll层层代理的实现的时候,说实话,我的脑海里立刻联想到了责任链模式。虽然接触过不少设计模式,但是笔者对责任链模式情有独钟,很多复杂的业务都是需要通过责任链进行拆分处理的。在笔者的一个支付类项目中,核心业务就是通过双向责任链构造的。核心服务类大概长这个样子(去掉了敏感的和与责任链模式不相干的东西):
// 核心服务类
public class BizService {
private String serviceId; // 服务id
private List<IProcessFilter> filters; // 业务过滤链
private IProcessFilter businessHandler; // 业务处理器,本质上是最后一个过滤器
/**
* 开始进行业务处理
*/
public void execute(BizContext context, IFilterCallback lastCallback)
throws ProcessException {
// 初始化责任链
LinkedList<IProcessFilter> filterList = new LinkedList<IProcessFilter>();
if (filters!= null) {
filterList.addAll(filters);
}
filterList.add(businessHandler);
FilterChain filterChain = new FilterChain(filterList);
try {
filterChain.process(context, lastCallback); // 开始责任链处理
} finally {
log.info("完成业务请求");
}
}
}
然后是责任链类:
public class FilterChain implements IFilterChain {
private LinkedList<IProcessFilter> filters; // 使用LinkedList保证执行顺序
public FilterChain(LinkedList<IProcessFilter> filters) {
this.filters = filters;
}
@Override
public void process(BizContext context, IFilterCallback callback)
throws ProcessException {
IProcessFilter nextFilter = filters.removeFirst(); // 取出第一个过滤器
nextFilter.process(context, callback, this);// 调用处理逻辑
}
}
一个简单的Filter大概就是下面这个样子:
@Slf4j
@Service
public class XxxFilter implements IProcessFilter {
@Override
public void process(BizContext context, final IFilterCallback callback,
IFilterChain filterChain) throws ProcessException {
// 过滤逻辑
.....
// 继续责任链处理
filterChain.process(context, new IFilterCallback() {
@Override
public void onPostProcess(ProcessContext context)
throws ProcessException {
// 回调逻辑
......
// 继续调用上个回调
callback.onPostProcess(context);
}
});
}
最后通过spring xml注入的方式,就可以组装一个完整的服务对象了。
上面的双向责任链实现,是科室的创始人写的(老大已经去了蚂蚁金服),很巧妙,但是也很复杂。特别是当Filter很多,每个Filter又有回调的时候,理解起来就没那么容易了。我曾经试图给同事讲清楚这个责任链,他听完后,仍然一脸懵逼。这个双向责任链有什么问题呢?目前我想到的有两个:
1-在处理完Filter逻辑后,必须显示的调用 filterChain.process()来让责任链继续执行下去。(这个问题并不好处理)
2-不管你有没有回调,你都必须显示的传递一个回调对象给下个Filter,因为上个Filter的回调需要你显示的去调用,也就是这里callback.onPostProcess(context)。
并不是所有的人都熟悉这个责任链,因此在做业务的时候,很有可能忘记调用callback.onPostProcess(context);导致程序出现莫名其妙的问题。笔者就曾因为忘记调用这个回调,导致出现乐观锁的问题,你说这是不是低级错误?
回到正题,通过mybatis的插件机制,对我们的双向责任链有什么启发呢?启发很大,插件的本质是拦截链,和我们过滤链是类似,我们同样可以对过滤器进行层层代理来达到双向责任链的效果!
首先是Filter的改造,之前的IProcessFilter只有一个process方法,现在我们新增一个回调方法:
/**
* Created by gameloft9 on 2020/4/14.
*/
public interface Filter {
// 过滤处理
default Object filter(Invocation invocation) throws Throwable{
return invocation.proceed(); // 默认不处理
};
default Object callBack(Invocation invocation) throws Throwable{
return invocation.getResult(); // 默认直接返回结果
};
}
然后是FilterChain,类似于mybatis的InterceptorChain,我们对filter进行层层代理:
/**
* Created by gameloft9 on 2020/4/14.
*/
public class LinkedFilterChain {
private LinkedList<Filter> filters; // 过滤器列表
public LinkedFilterChain(){
this.filters = new LinkedList<Filter>();
}
public LinkedFilterChain(LinkedList<Filter> list){
filters = list;
}
public void addAll(LinkedList<Filter> list){
filters.addAll(list);
}
/**
* 代理目标对象
* @param target
* @return
*/
public Object proxy(ProcessContext context,Object target) {
return FilterInvocationHandler.wrapAll(context,target,filters);
}
}
最后是InvocationHandler的实现类,负责具体的代理包装,业务调用:
/**
* InvocationHandler实现类
* Created by gameloft9 on 2020/4/13.
*/
public class FilterInvocationHandler implements InvocationHandler {
private final Object target; // 被代理对象
private final Filter filter; // 过滤器
private final ProcessContext context; // 业务的上下文
public FilterInvocationHandler(ProcessContext context, Object target, Filter filter) {
this.context = context;
this.target = target;
this.filter = filter;
}
/**这里invoke写比较简单,没有过滤掉一些通用的方法*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(context, target, method, args);
// 进行过滤
filter.filter(invocation);
// 回调
return filter.callBack(invocation);
}
/**
* 包装代理
*/
public static Object wrap(ProcessContext context, Object target, Filter filter) {
Class type = target.getClass();
Class[] interfaces = target.getClass().getInterfaces();
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new FilterInvocationHandler(context, target, filter));
}
return target;
}
/**
* 层层代理
* @param target
* @param filters
* @return
*/
public static Object wrapAll(ProcessContext context, Object target, LinkedList<Filter> filters) {
for (Filter filter : filters) {
target = wrap(context, target, filter);
}
return target;
}
}
然后我们对Invocation对象做了扩展,将业务的上下文和执行结果保存下来,方便后面使用。
/**
* 封装一下method.invoke
* 而且还可以放更多有用的对象,例如context对象。
* Created by gameloft9 on 2020/4/13.
*/
@Data
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
private Object result; // 执行结果
private final ProcessContext context; // 业务的上下文
public Invocation(ProcessContext context,Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
this.context = context;
this.result = null;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
Object result = method.invoke(target,args);
this.result = result; // 保存一下执行结果,方便后面获取
return result;
}
}
业务上下文可以根据自己的业务需求来定制,这里只给一个简单的例子:
/**
* 上下文
* Created by gameloft9 on 2020/4/14.
*/
@Data
public class ProcessContext {
/**
* 服务开始时间(nano)
*/
private long serviceAcceptNanoTime = System.nanoTime();
/**
* 请求原文
*/
private String jsonRequest;
/**
* 应答原文
*/
private String jsonResponse;
// 还可以放一些通用的东西
// ......
/**
* 存放额外的数据
* */
private Map<String,String> map = new HashMap<>();
public ProcessContext(){
}
}
到这里我们的双向责任链就完成了!我们试试如何使用它。
/**
* 基于jdk动态代理的双向责任链
* 不同于普通的双向责任链
* Created by gameloft9 on 2020/4/13.
*/
@Slf4j
public class Client {
public static void main(String[] args) {
// 拿到过滤列表
Filter filterOne = new FilterOne();
Filter filterTwo = new FilterTwo();
LinkedList<Filter> list = new LinkedList<>();
list.add(filterTwo);
list.add(filterOne);
// 生成责任链
LinkedFilterChain filterChain = new LinkedFilterChain(list);
// 创建业务上下文
ProcessContext context = new ProcessContext();
Request request = new Request(5,3);
context.setJsonObjectRequest((JSONObject) JSON.toJSON(request));
// 代理最终业务目标,业务里面做加法操作,很简单就不贴代码了
Service service = (Service) filterChain.proxy(context,new ServiceImpl());
// 执行业务
int result = service.doService(context);
log.info("执行结果:{}",result);
// 看下Context
log.info("context:{}",context);
}
}
我们看看Filter怎么写的:
/**
* Created by gameloft9 on 2020/4/14.
*/
@Slf4j
public class FilterOne implements Filter {
@Override
public Object filter(Invocation invocation) throws Throwable {
log.info("Filter One 处理逻辑..");
// 处理context
ProcessContext context = invocation.getContext();
context.getMap().put("preOne","preOne");
// 继续调用
return invocation.proceed();
}
@Override
public Object callBack(Invocation invocation) throws Throwable {
// 操作结果
Object result = invocation.getResult();
log.info("Filter One 对结果进行+1,result:{}", result = (Integer)result + 1);
// 往context里面塞点东西
ProcessContext context = invocation.getContext();
context.getMap().put("afterOne","afterOne");
return result;
}
}
从这里看出,虽然执行过滤逻辑后,仍然要调用invocation.proceed(),但是我们的回调变得简单了,不需要塞内部匿名类,也不需要担心忘记调用callback.postProcess。而且Filter给出了默认实现,因此如果不需要处理过滤或者回调,连相应的方法也不需要实现和重写!
下面是执行结果:
根据这个责任链,我们还可以对我们BizService进行重构,限于篇幅这里就不贴代码了。
Mybatis通过jdk动态代理,可以为ParamterHandler、Executor、StatementHandler、ResultSetHandler四个对象添加拦截器,灵活的进行一些业务处理。不过,任何事情都有两面性,如果你了解插件的机制,那么可以做很多很棒的事情,例如分页,查询结果转换。如果不了解,那么你的插件很有可能破坏mybatis的内部结构。