在MyBatis中使用插件,我们必需实现Interceptor,让我们看看它的定义和各个方法的含义。
Interceptor.java
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
在接口中有3个方法,我们来看看他们的含义。
- intercept方法:它将直接覆盖你所拦截对象的原有方法,因为它是插件的核心方法,intercept里有个参数Invocation对象,通过它可以反射调试原来对象的方法,我们稍后讨论它的设计和使用。
- plugin方法:target里被拦截的对象,它的作用是给被拦截的对象生成一个代理对象,并返回它,为了方便MyBatis使用org.apache.ibatis.plugin.Plugin中的wrap静态(static)方法提供生成的代理对象,我们往往使用plugin方法便可以生成一个代理对象了,当然你也可以自定义,自定义去实现的时候,需要特别小心。
- setProperties方法,允许在plugin元素中配置所需要的参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面再取出 。
这里我们看到了插件的架子,这样的模式我们称为模板模式,就是提供了一个架子,并在架子中定义方法是用来做什么的,在实际中,我们常用模板模式。
插件的初始化是在MyBatis初始化的时候完成,这点我们通过XMLConfigBuilder的代码就可以知道。
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); } } }
那使用又是如何使用的呢?
<plugins> <plugin interceptor="com.spring_101_200.test_131_140.test_133_mybatis_lazyloadingenabled_aggressivelazyloading.DataScopeInterceptor"> <property name="someProperty" value="100"/> </plugin> </plugins>
在解析配置文件的时候,在MyBatis的上下文初始化过程中,就开始读入插件节点和配置参数,同时使用反射技术生成对应的插件,然后调用插件方法中的setProperties方法,设置我们的配置参数,然后将插件实例保存到配置对象中,以便读取和使用它,所以插件的实例对象是一开始就被初始化的,而不是到使用的时候才初始化,我们使用它的时候,直接拿出来就可以了,这样有助于提高性能。
Configuration.java
public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
interceptorChain在Configuration里面是一个属性,它里面只有一个addInterceptor方法。
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); } }
插件的代理和反射设计。
插件的是责任链模式,首先什么是责任链模式呢?就是一个对象,在MyBatis中可能是四大对象中的一个,在多个角色中传递,处在传递链上的任何一个角色都有处理它的机会,这个还是很抽象,打个比方,你在公司中是个重要人物,你需要请假三天,那么请假的流程是,首先你需要项目经理批准,然后部门经理批准,最后总裁批准才能完成,你的请假就是一个对象,它经过经理,部门经理,总裁多个角色审批处理,每个角色都可以对你的请假请求作出修改和批示,这就是责任链模式,它的作用是让每一个责任链上的角色都有机会去拦截这个对象,在将来如果有新的角色可以轻松拦截请求对象,进行处理。
MyBatis的责任链是由interceptorChain去定义,不知道读者是否记得MyBatis在创建执行器时调用这样的代码。
Configuration.java
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); return statementHandler; }
我们都知道plugin方法是生成代理对象的就去,它取出插件的时候是从Configuration对象中去取出,从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法然后返回一个代理,如果存在第二个插件,那么我们拿到第一个代理对象,传递给plugin方法再返回第一个代理对象的代理 … 依此类推,有多少个拦截器就生成多少个代理对象,这样每一个插件都子可以拦截到真实的对象了,这就好比每一个插件都可以一层层的处理被拦截的对象,其实读者只要认真的阅读MyBatis的源码,就可以发现MyBatis的四大对象也是这样处理的。
如果我们自己编写代理类觉得工作量大,为此MyBatis中提供了一个常用的工具类,用来生成代理对象,它便是Plugin类,Plugin类实现了InvocationHandler接口,采用JDK动态代理。我们来看看这个类两个十分重要的方法。
public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; private Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } 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; } 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); } } private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { // issue #251 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
我们看看它是一个动态代理对象,其中wrap方法为我们生成这个对象的动态代理对象。
我们再来看看invoke方法,如果你使用这个类为插件生成代理对象,那么代理对象在调用方法的时候就会进入到invoke方法中,在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会被我们在这里调用,然后就返回结果,如果不存在签名方法,那么直接反射调试我们挂靠的方法。
我们创建一个invocation对象,其构造方法的参数包括被代理的对象,方法及参数incocation对象进行初始化,它有一个proceed()方法。
Invocation.java
public class Invocation { private Object target; private Method method; private Object[] args; public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } public Object getTarget() { return target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }
这个方法是调试被代理对象的真实方法,现在假设有n个插件,我们知道第一个传递的参数是四大对象的本身,然后调用一次wrap方法第一个代理对象,而这里的反射就是反射的四大对象本身的真实方法,如果有第二个插件,我们会将第一个代理对象传递给wrap方法,生成第二个代理对象,这里反射就是指第一个代理对象的invoke方法,依此类推直到最后一个代理对象,如果每一个代理对象都调用这个proceed方法,那么最后四大对象本身的方法也会被调用,只是它会从最后一个代理对象的invoke方法运行到第一个代理对象的invoke方法,直到四大对象的真实方法。
在初始化的时候,我们一个个加载插件实例,并用setProperties()方法进行初始化,我们可以使用MyBatis提供的Plugin.wrap方法生成代理对象,再一层层的使用Invocation的proceed()方法来推动代理对象的运行,所以在多个插件的环境下,调试proceed()方法时,MyBatis总是最后一个代理对象运行到第一个代理对象,最后真实被拦截的对象方法被运行,大部分情况下,使用MyBatis的Plugin类生成代理对象足够我们使用,当然你觉得可以自己写规则,也可以不用这个类,我们必需慎之又慎的使用这个方法,因为它覆盖底层的方法。
常用的工具类-MetaObject
在编写插件之前我们需要去学习一个MyBatis的工具类-MetaObject,它可以有效读取或修改一些重要的对象的属性,在MyBatis中,四大对象给我们提供了public设置方法很少,我们难以通过其自身得到相关的属性信息,但是有了MetaObject这个工具类,我们就可以通过其他的技术手段来读取或修改这些重要的对象的属性,在MyBatis插件中它是一个十分常用的工具类。
- MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) :方法用于包装对象,这个方法我们己经不再使用了,而是用MyBatis为我们提供SystemMetaObject.forObject(Object obj);
- Object getValue(String name):方法用于获取对象的属性值,支持OGNL
- public void setValue(String name, Object value) :方法用于修改对象的属性值,支持OGNL
在MyBatis对象中大量的使用了这个类进行包装,包括四大对象,使得我们可以通过它来给四大对象的某些属性赋值从而满足我们的需要。
public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); //进行绑定 //分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过循环可以分离出最原始的目标类) while(metaObject.hasGetter("h")){ Object object = metaObject.getValue("h"); metaObject = SystemMetaObject.forObject(object); } //BoundSql对象是处理SQL语句用的 String sql = (String)metaObject.getValue("delegate.boundSql.sql"); //判断SQL是否是select语句,如果不是select语句,那么就出错 //如果是,则修改它,最多返回1000行,这里用的是MySQL数据库,其他数据库要改写成其他的 if(sql != null && sql.toUpperCase().trim().indexOf("select") == 0 ){ //通过SQL重写来实现,这里我们起了一个奇怪的别名,避免与表名重复 sql = "select * from (" + sql + " ) $_$limit_$table_limit 1000"; metaObject.setValue("delegate.boundSql.sql",sql); } Object result = invocation.proceed(); return result; }
在前面的博客中我们知道StatementHandler实际上是RountingStatementHandler对象,它的delegate属性才是真实的服务StatementHandler,真实的StatementHandler有一个属性BoundSql,它下面又有一个属性sql,所以才有了路径delegate.boundSql.sql,我们就可以通过这个路径去获取或修改对应的运行的SQL,通过这样的改写,就可以限制所有的查询SQL都只能返回1000行记录了。
插件的开发过程和实例。
有了对插件的理解,我们再学习插件的使用就更加容易了,例如,开发一个互联网项目需要去限制每一条SQL返回的行数,限制行数的需求是个可梳理一下轩的参数,业务还可以根据自己的需要去配置,这样很有必要,因为大型互联网系统一旦同时传输大量的数据很容易宕机,这里多们可以通过修改SQL来完成。
确定需要拦截的签名
正如MyBatis插件可以拦截四大对象中的任意一个,从Plugin源码中我们可以看到它的注册签名才能够运行插件,签名需要确定一些要素。
1. 确定需要拦截的对象
首先根据功能来确定你需要拦截的对象
- Executor是执行SQL的全部过程,包括组装参数,组装结果集返回和执行SQL过程,都可以拦截,较为广泛,我们一般不算太多。
- StatementHandler是执行SQL的过程中,我们可以重写 执行SQL的过程,这是我们常用拦截对象
- ParameterHandler,很明显它主要的拦截执行SQL参数组装 ,你可以重写组装参数规则。
- ResultSetHandler用于拦截执行结果组装,你可以重写组装结果规则。
我们清楚需要拦截的StatementHandler对象,应该在编译SQL之前,修改SQL使得结果返回数量被限制。
拦截方法和参数
当你确定需要拦截什么对象,接下来就要确定需要拦截什么方法及方法参数,这些需要你理解MyBatis四大对象动作基础上才能确定。
查询的过程就是通过Executor调度Statement来完成的,调试StatementHandler的prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在此之前完成的SQL需要重新编写。让我们 看看StatementHandler接口定义。
StatementHandler.java
public interface StatementHandler { Statement prepare(Connection connection) throws SQLException; void parameterize(Statement statement) throws SQLException; void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
以上任何方法可以被拦截。从接口定义而言,prepare方法有一个参数Connection对象。
@Intercepts({@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class})})
public class DataScopeInterceptor extends SqlParserHandler implements Interceptor {
…
}
其中Intercepts说明它是一个拦截器,@Signature是注册拦截器签名的地方,只有签名满足条件才能拦截,type可以是四大对象中的一个,这里的StatementHandler,method代表要拦截的四大对象的某种接口方法,而args则表示方法的参数,你需要根据拦截器对方法参数进行设置。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class DataScopeInterceptor extends SqlParserHandler implements Interceptor { /** * 代替拦截对象的方法内容 * 责任链对象 */ @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); this.sqlParser(metaObject); // 先判断是不是SELECT操作 BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); String originalSql = boundSql.getSql(); System.out.println(originalSql); //如果当前代理的是一个非代理对象,那么它就回调用真实拦截器对象方法,如果不是,它会调度下个插件代理对象的invoke方法。 Object result = invocation.proceed(); return result; } /** * 生成拦截对象的代理 * * @param target 目标对象 * @return 代理对象 */ @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { //使用MyBatis提供的Plugin类生成代理对象 return Plugin.wrap(target, this); } return target; } /** * @param properties mybatis获取插件的属性,我们在MyBatis配置文件里配置的 */ @Override public void setProperties(Properties properties) { } }
我们需要在MyBatis文件里面配置才能够使用插件,配置mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="spring_101_200/config_121_130/spring127_mybatis_plugins/db.properties"></properties> <settings> <setting name="cacheEnabled" value="false"/> <setting name="useGeneratedKeys" value="true"/> <setting name="defaultExecutorType" value="REUSE"/> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> <typeAliases> <package name="com.spring_101_200.test_121_130.test_127_mybatis_plugins"/> </typeAliases> <plugins> <plugin interceptor="com.spring_101_200.test_121_130.test_127_mybatis_plugins.DataScopeInterceptor"> <property name="someProperty" value="100"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="jdbc"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"></property> <property name="username" value="${db.username}"></property> <property name="password" value="${db.pwd}"></property> </dataSource> </environment> </environments> <mappers> <mapper resource="spring_101_200/config_121_130/spring127_mybatis_plugins/UserMapper.xml"></mapper> </mappers> </configuration>
插件实例
有了上面的知识来实现插件就容易从了,在一个大型的互联网系统中,我们使用的是MySQL数据库,我们有一张表,在user表中有一个is_delete字段,但是有些同学在写sql的时候总是忘记写is_delete了,导致本来被删除的数据,总是被查询出来,那我们来写一个插件,实现如果用户没有写is_delete=0,帮用户加上这个字段吧。
首先我们先确定需要拦截的四大对象中的哪一个,根据功能我们需要修改SQL的执行,SqlSession运行原理告诉我们需要拦截的是StatementHandler对象,因为是由它的prepare方法来预编译的SQL语句,我们可以在预编译前修改语句来满足我们的需要,所以我们选择拦截器StatementHandler的prepare()方法,在它预编译前,需要重写SQL,以达到要求的结果,它是一个参数(Connection connection),所以我们就很轻易的得到签名注解,其实现方法如下:
- 准备POJO
@Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String password; private String realName; private Long managerId; }
- 准备UserMapper
public interface UserMapper { User getUser(Long id); }
- 准备UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.spring_101_200.test_151_160.test_154_mybatis_plugins.UserMapper" > <select id="getUser" resultType="User" > select * from lz_user where id = #{id} </select> </mapper>
- 准备拦截器
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class QueryInterceptor extends SqlParserHandler implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaStmtHandler = SystemMetaObject.forObject(statementHandler); //分离代理对象,从而形成多次代理,通过两次循环最原始的被代理的类,MyBatis使用的是JDK代理 while (metaStmtHandler.hasGetter("h")){ Object object = metaStmtHandler.getValue("h"); metaStmtHandler = SystemMetaObject.forObject(object); } //分离最后一个代理对象的目标类 while (metaStmtHandler.hasGetter("target")){ Object object = metaStmtHandler.getValue("target"); metaStmtHandler = SystemMetaObject.forObject(object); } //取出即将要执行的SQL String originalSql = (String)metaStmtHandler.getValue("delegate.boundSql.sql"); //这只是一个简单的处理,现实情况要复杂得多 if(!originalSql.contains("is_delete") && !originalSql.contains(" left ") && !originalSql.contains(" inner ")){ originalSql = originalSql + " and is_delete = 0 "; } System.out.println("=========>>>>>>>>>>>>> " + originalSql); 重写要执行的SQL metaStmtHandler.setValue("delegate.boundSql.sql",originalSql); //调用原来的对象方法,进行责任链的下一层级 return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { //使用默认的MyBatis提供类生成代理对象 return Plugin.wrap(target, this); } return target; } /** * @param properties mybatis配置的属性 */ @Override public void setProperties(Properties properties) { } }
- 全局配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="spring_101_200/config_151_160/spring154_mybatis_plugins/db.properties"></properties> <settings> <setting name="cacheEnabled" value="false"/> <setting name="useGeneratedKeys" value="true"/> <setting name="defaultExecutorType" value="REUSE"/> <setting name="mapUnderscoreToCamelCase" value="true" /> <setting name="logImpl" value="STDOUT_LOGGING" /> </settings> <typeAliases> <typeAlias type="com.spring_101_200.test_151_160.test_154_mybatis_plugins.User" alias="User"></typeAlias> </typeAliases> <plugins> <plugin interceptor="com.spring_101_200.test_151_160.test_154_mybatis_plugins.QueryInterceptor"> <property name="someProperty" value="100"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="jdbc"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"></property> <property name="username" value="${db.username}"></property> <property name="password" value="${db.pwd}"></property> </dataSource> </environment> </environments> <mappers> <mapper resource="spring_101_200/config_151_160/spring154_mybatis_plugins/UserMapper.xml"></mapper> </mappers> </configuration>
- 测试
@Test public void testGetUser() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(456l); System.out.println(JSON.toJSONString(user)); }
结果如果没有查到。
总结 :
结束之前,请大家注意以下6点:
- 能不使用插件的尽量不要使用插件,因为它将修改MyBatis的底层设计
- 插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,所以减少插件就能减少代理,从而提供系统性能。
- 编写插件需要了解MyBatis的运行原理,了解四大对象及其方法的作用,准确判断需要拦截什么对象,什么方法,参数是什么?才能确定签名如何编写。
- 在插件中往往需要读取和修改MyBatis映射器中对象属性,你需要熟练掌握MyBatis映射器内部组成的知识。
- 插件的代码编写要考虑全面,特别是多个插件层层代理的时候,需要保存逻辑的正确性。
- 尽量少改动MyBatis底层的东西,减少错误的发生。