Mybatis插件机制探究及延伸思考

文章目录

      • 1、前言
      • 2、使用示例
      • 3、原理分析
      • 4、延伸思考
        • 4.1、关于plugin方法
        • 4.2、责任链模式?
          • 4.2.1、基于jdk动态代理的双向责任链
      • 5、总结

1、前言

Mybatis提供了插件机制,可以让我们介入到底层执行的一些流程,例如SQL执行前打印下SQL语句。这里的插件,在mybatis里面实际上是拦截器。在深入探究之前,先看看如何使用,然后再分析原理,最后会提一下笔者从里面得到的一些启发。

2、使用示例

首先我们需要在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)方法。

3、原理分析

进行原理分析前,我们脑海里先问几个问题:

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方法:Mybatis插件机制探究及延伸思考_第1张图片
我们发现,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的插件机制就探究完了。但是笔者的思考并没有跟着结束,我看完后思考了两个问题:

4、延伸思考

4.1、关于plugin方法

在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;
  }

这样可以减少一个接口方法,插件类也不用关心如何去做动态代理了。那么这里为什么要把动态代理的过程暴露给插件实现类呢,我想可能作者有什么考量吧?在代理目标对象前,可能需要对其进行一些逻辑处理?

4.2、责任链模式?

当我看到那个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的插件机制,对我们的双向责任链有什么启发呢?启发很大,插件的本质是拦截链,和我们过滤链是类似,我们同样可以对过滤器进行层层代理来达到双向责任链的效果!

4.2.1、基于jdk动态代理的双向责任链

首先是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给出了默认实现,因此如果不需要处理过滤或者回调,连相应的方法也不需要实现和重写!

下面是执行结果:
Mybatis插件机制探究及延伸思考_第2张图片
根据这个责任链,我们还可以对我们BizService进行重构,限于篇幅这里就不贴代码了。

5、总结

Mybatis通过jdk动态代理,可以为ParamterHandler、Executor、StatementHandler、ResultSetHandler四个对象添加拦截器,灵活的进行一些业务处理。不过,任何事情都有两面性,如果你了解插件的机制,那么可以做很多很棒的事情,例如分页,查询结果转换。如果不了解,那么你的插件很有可能破坏mybatis的内部结构。

你可能感兴趣的:(MyBatis)