Mybatis插件
Mybatis对(Executor,StatementHandler,ParameterHandler,ResultSetHandler)四大组件提供了简单易用的插件扩展机制。支持插件对四大核心对象进行拦截处理,对Mybatis来说插件就是拦截器,他底层使用了动态代理来实现插件机制,换句话说,Mybatis的四大组件都是代理对象。
允许使用插件来拦截的方法调用包括:
- 执行器Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- 参数处理器ParameterHandler (getParameterObject, setParameters)
- 结果集处理器ResultSetHandler (handleResultSets, handleOutputParameters)
- SQL语法构建器StatementHandler (prepare, parameterize, batch, update, query)
Mybatis插件原理
四大对象创建的时候:
- 每个创建出来的对象不是直接返回,而是通过
interceptorChain.pluginAll(parameterHandler)
生成代理对象, interceptorChain.pluginAll
方法做了什么呢,他从SqlMapConfig.xml
中注册的plugin
中找到自己的拦截器,并通过interceptor.plugin(target)
返回target的包装对象;- 最后实际调用的是拦截器,我们就可以在拦截器里面,做一些自定义处理实现插件;
实现自定义插件例子
- 创建自定义插件类,
// 告诉Mybatis这是个插件 @Intercepts({ // 告诉Mybatis这个插件具体拦截目标 @Signature( // 指定拦截类 type = Executor.class, // 指定拦截方法 method = "query", // 指定拦截方法的参数(兼容重载) args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) // 这里可以添加多个拦截目标 }) public class MyPlugin implements Interceptor { /** * 这里写具体的拦截业务逻辑 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 代理的目标对象 final Object target = invocation.getTarget(); // 执行方法 final Method method = invocation.getMethod(); // 参数 final Object[] args = invocation.getArgs(); System.out.println("拦截之前:"); System.out.println("拦截目标对象:" + target.getClass()); System.out.println("拦截目标方法:" + method.getName()); System.out.println("拦截方法调用参数" + args); // 前置处理流程在此处之前写 // final Object proceed = invocation.proceed(); 默认处理 final Object proceed = method.invoke(target, args); // 后置处理逻辑在此处之后写 System.out.println("拦截之后:"); // 返回处理的对象 return proceed; } /** * * 这里写注册插件逻辑,不写也可以,接口有默认实现 * * default Object plugin(Object target) { * * return Plugin.wrap(target, this); * * } * * @param target 需要代理的对象 * @return */ @Override public Object plugin(Object target) { // 这里添加自定义处理 System.out.println("注册插件"); /* 如果有多个插件代理相同目标,target 可能是上一个插件的代理对象,所以必须调用 Plugin.wrap(target, this); 如果直接返回新对象,则可能丢失之前的插件性能 */ return Plugin.wrap(target, this); } /** * 这里能获取配置文件里配置的自定义属性 */ @Override public void setProperties(Properties properties) { System.out.println("获取自定义属性:" + properties); } }
SqlMapConfig.xml
中注册插件<plugins> <!--interceptor是自定义插件类的全限定名--> <plugin interceptor="com.wjy.plugin.MyPlugin"> <!--这里添加自定义属性--> <property name="test" value="测试属性"/> </plugin> </plugins>
- 测试方法
@Test public void testPlugin() throws IOException { final SqlSession sqlSession = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsReader("SqlMapConfig.xml")) .openSession(); final AnnotationOrderDao orderDao = sqlSession.getMapper(AnnotationOrderDao.class); final List<Order> list = orderDao.list(); System.out.println(list); }
- 执行测试数据结果
为什么注册了四次插件?因为Mybatis对四大对象都做了代理,注册的时候,具体区分给那个对象代理是在获取自定义属性:{test=测试属性} # 加载程序时获取自定义属性 注册插件 # 这里是Executor对象注册插件 拦截之前: 拦截目标对象:class org.apache.ibatis.executor.CachingExecutor 拦截目标方法:query 拦截方法调用参数[Ljava.lang.Object;@1fa1cab1 注册插件 # 其他三大对象之一注册对象 注册插件 # 其他三大对象之一注册对象 注册插件 # 其他三大对象之一注册对象 Preparing: select * from orders # 执行SQL 拦截之后: [order{id=1, ordertime='2019-12-12', total=3000, user=null}, order{id=2, ordertime='2019-12-12', total=4000, user=null}, order{id=3, ordertime='2019-12-12', total=5000, user=null}, order{id=4, ordertime='2019-12-13', total=5001, user=null}]
Plugin.wrap(target, this);
里面判断的,所以同一个插件会执行四次注册;
源码分析
- Mybatis创建执行器对象源码
Configuration.java
:public Executor newExecutor(Transaction transaction, ExecutorType executorType) { ...省略代码 // 这里调用interceptorChain.pluginAll生成执行器代理对象 return (Executor) interceptorChain.pluginAll(executor); }
interceptorChain.pluginAll
源码// target 为需要代理的四大对象。 public Object pluginAll(Object target) { // interceptors为配置文件配置的插件 for (Interceptor interceptor : interceptors) { // 循环注册插件,生成代理对象(interceptor.plugin这个方法我们在写插件的时候,可以重写) target = interceptor.plugin(target); } return target; }
interceptor.plugin
源码,interceptor.plugin这个方法我们在写插件的时候,可以重写default Object plugin(Object target) { // 调用Plugin.wrap具体注册逻辑 return Plugin.wrap(target, this); }
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; }
- 插件拦截具体执行逻辑
Plugin.invoke
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); } }
总结
实现一个Mybatis插件需要这几个步骤:
- 自定义插件类实现
Interceptor
接口,使用@Intercepts
声明插件,指定拦截目标 - 配置文件添加插件配置
- 启动程序,插件生效