本篇文章是「深刻浅出MyBatis:技术原理与实践」书籍的总结笔记。java
上一篇介绍了 MyBatis解析和运行原理 ,包括SqlSessionFactory的构建和SqlSession的执行过程,其中,SqlSession包含四大对象,能够在四大对象调度的时候插入自定义的代码,以知足特殊的需求,这即是MyBatis提供的插件技术。mysql
有些特殊场景,须要使用插件统一处理,好比:在进行多租户开发时,数据要按租户隔离,能够在sql语句后面统一添加租户编号筛选条件。sql
本篇就来介绍下插件,经过本篇的介绍,你会了解到:微信
在MyBatis中使用插件,须要实现Interceptor接口,定义以下:mybatis
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
详细说说这3个方法:ide
插件的初始化时在MyBatis初始化的时候完成的,读入插件节点和配置的参数,使用反射技术生成插件实例,而后调用插件方法中的setProperties方法设置参数,并将插件实例保存到配置对象中,具体过程看下面代码。工具
plugin配置示例以下:性能
<plugins> <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin"> <property name="dbType" value="mysql"/> </plugin> <plugins>
插件初始化过程:ui
public class XMLConfigBuilder extends BaseBuilder { ...... private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } } ...... }
配置对象Configuration的添加插件方法:插件
public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } }
InterceptorChain是一个类,主要包含一个List属性,保存Interceptor对象:
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
插件用的是责任链模式,责任链模式是一种对象行为模式。在责任链模式里,不少对象由每个对象对其下家的引用而链接起来造成一条链,请求在这个链上传递,直到链上的某一个对象决定处理此请求。
前面提到了InterceptorChain类,其中有个pluginAll方法,责任链就是在该方法定义的。
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
上面介绍过plugin方法,它是生成代理对象的方法,从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法,返回一个代理;若是存在第二个插件,就拿着第一个代理对象,传递给plugin方法,返回第一个代理对象的代理.....
plugin方法是须要咱们去实现的,如何生成代理类呢,MyBatis提供了Plugin工具类,它实现了InvocationHandler接口(JDK动态代理的接口),看看它的2个方法:
public class Plugin implements InvocationHandler { 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; } @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); } } }
分析下这块代码,Plugin提供了静态方法wrap方法,它会根据插件的签名配置,使用JDK动态代理的方法,生成一个代理类,当四大对象执行方法时,会调用Plugin的invoke方法,若是方法包含在声明的签名里,就会调用自定义插件的intercept方法,传入Invocation对象。
另外,Invocation对象包含一个proceed方法,这个方法就是调用被代理对象的真实方法,若是有n个插件,第一个传递的参数是四大对象自己,而后调用一次wrap方法产生第一个代理对象,这里的反射就是四大对象的真实方法,若是有第二个插件,这里的反射就是第一个代理对象的invoke方法。
因此,在多个插件的状况下,调度proceed方法,MyBatis老是从最后一个代理对象运行到第一个代理对象,最后是真实被拦截的对象方法被执行。
MetaObject是MyBatis给咱们提供的工具类,它能够有效的获取或修改一些重要对象的属性。
举例说明,咱们拦截StatementHandler对象,首先要获取它要执行的SQL,添加返回行数限制。
编写一个自定义插件,实现intercept方法,方法实现以下
StatementHandler statementHandler=(StatementHandler)invocation.getTarget(); MetaObject metaObj=SystemMetaObject.forObject(statementHandler); //获取sql String sql=(String)metaStatementHandler.getValue("delegate.bound.sql"); //添加limit条件 sql="select * from (" + sql + ") limit 1000"; //从新设置sql metaStatementHandler.setValue("delegate.bound.sql",sql);
最后总结下插件的开发步骤。
好比想拦截StatementHandler对象的prepare方法,该方法有一个参数Connection对象,能够这样声明:
@Intercepts({ @Signature(type =StatementHandler.class, method="prepare" , args={Connection.class})}) public class MyPlugin implements Interceptor{ ...... }
上面已经分析过原理,实现Interceptor接口的方法便可,经过Plugin工具类方便生成代理类,经过MetaObject工具类方便操做四大对象的属性,修改对应的值。
最后配置自定义的插件:
<plugins> <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin"> <property name="dbType" value="mysql"/> </plugin> <plugins>
自定义插件仍是比较复杂的,若是不了解原理,很容易出错,能不用插件尽可能不要使用,由于它是修改MyBatis的底层设计。 插件生成的是层层代理对象的责任链模式,经过反射方法运行,性能不高,要考虑全面,特别是多个插件层层代理的逻辑。
下一篇会介绍MyBatis与Spring的集成。
欢迎扫描下方二维码,关注个人我的微信公众号 ~