MyBatis系列七 插件
第6章讨论了四大对象的运行过程,在Configuration对象的创建方法里我们看到了 MyBatis用责任链去封装它们。换句话说,我们有机会在四大对象调度的时候插入我们的代码去执行一些特殊的要求以满足特殊的场景需求,这便是MyBatis的插件技术。
在没能完全理解插件的时候谈论插件是十分危险的。使用插件就意味着在修改MyBatis的底层封装,它给予我们灵活性的同时,也给了我们毁灭MyBatis框架的可能性,操作不慎有可能摧MyBatis框架,只有掌握了 MyBatis的四大对象的协作过程和插件的实现原理,你才能构建出安全高效的插件,所以笔者在完成第6章的基础上,在这里详细讨论插 件的设计和应用。
万事开头难,我们从插件的基本概念开始。需要再次提醒大家的是插件很危险,能不使用尽量不要使用,非要使用时请慎重使用。
一、插件接口
在MyBatis中使用插件,我们就必须实现接口 Interceptor,让我们先看看它的定义和各个方法的含义,如代码清单7.1所示。
代码清单 7-1: Interceptor.java
public interface Interceptor ( Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target);
void setProperties(Properties properties);
}
在接口中,运用了 3个方法,让我们先掌握这3个方法的含义。
-
- intercept方法:它将直接覆盖你所拦截对象原有的方法,因此它是插件的核心方法。 intercept里面有个参数Invocation对象,通过它可以反射调度原来对象的方法,我 们稍后讨论它的设计和使用。
- plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。为 了方便 MyBatis 使用 org.apache.ibatis.plugin.Plugin 中的 wrap 静态(static) 方法提供生成代理对象,我们往往使用plugin方法便可以生成一个代理对象了。当 然你也可以自定义。自定义去实现的时候,需要特别小心。
- setProperties方法:允许在plugin元素中配置所需参数,方法在插件初始化的时候 就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。
这里我们看到了插件的骨架,这样的模式我们称为模板(template)模式,就是提供一个 骨架,并且告知骨架中的方法是干什么用的,由开发者来完成它。在实际中,我们常常用 到模板模式。
二、插件的初始化
插件的初始化是在MyBatis初始化的时候完成的,这点我们通过XMLConfigBuilder 中的代码便可知道,如代码清单7.2所示。
代码清单7-2:插件初始化
private void pluginElement(XNode parent) throws Exception ( if (parent != null) ( for (XNode child : parent.getChildren()) ( String interceptor = child.getStringAttribute(Hinterceptorn);
Properties properties = child.getChildrenAsProperties ();
Interceptor interceptorInstance = (Interceptor) resolveClass (interceptor).newlnstance(); interceptorinstance.setProperties(properties);
configuration.addinterceptor(interceptorInstance); } }
在解析配置文件的时候,在MyBatis的上下文初始化过程中,就开始读入插件节点和 我们配置的参数,同时使用反射技术生成对应的插件实例,然后调用插件方法中的 setProperties方法,设置我们配置的参数,然后将插件实例保存到配置对象中,以便读取和 使用它。所以插件的实例对象是一开始就被初始化的,而不是用到的时候才初始化的,我 们使用它的时候,直接拿出来就可以了,这样有助于性能的提高。
我们再来看看插件在Configuration对象里是怎样保存的’如代码清单7-3所示。
interceptorChain 在 Configuration 里面是一个属性,它里面有个 addinterceptor 方法,如 代码清单7.4所示
显然,完成初始化的插件就保存在这个List对象里面等待将其取出使用。
三、插件的代理和反射设计
插件用的是责任链模式。首先什么是责任链模式,就是一个对象,在MyBatis中可能 是四大对象中的一个,在多个角色中传递,处在传递链上的任何角色都有处理它的机会。 这句话还是很抽象,打个比方,你在公司中是个重要人物,你需要请假3天。那么,请假 流程是,首先你需要项目经理批准,然后部门经理批准,最后总裁批准才能完成。你的请 假请求就是一个对象,它经过项目经理、部门经理、总裁多个角色审批处理,每个角色都 可以对你的请假请求作出修改和批示。这就是责任链模式,它的作用是让每一个在责任链 上的角色都有机会去拦截这个对象。在将来如果有新的角色也可以轻松拦截请求对象,进 行处理。
MyBatis的责任链是由interceptorChain去定义的,不知道读者是否记得MyBatis在创 建执行器时用到过这样的代码。
我们不妨看看pluginAll()方法是如何实现的,如代码清单7-5所示。
我们知道plugin方法是生成代理对象的方法,当它取出插件的时候是从Configuration 对象中去取出的。从第一个对象(四大对象中的一个)开始,将对象传递给了 plugin方法, 然后返回一个代理;如果存在第二个插件,那么我们就拿到第一个代理对象,传递给plugin 方法再返回第一个代理对象的代理 .....依此类推,有多少个拦截器就生成多少个代理对象。这样每一个插件都可以拦截到真实的对象了。这就好比每一个插件都可以一层层处理被拦 截的对象。其实读者只要认真阅读MyBatis的源码,就可以发现MyBatis的四大对象也是 这样处理的。
如果要我们自己编写代理类,工作量会很大,为此MyBatis中提供了一个常用的工具 类,用来生成代理对象,它便是Plugin类。Plugin类实现了 InvocationHandler接口,釆用 的是JDK的动态代理,我们先看看这个类的两个十分重要的方法,如代码清单7.6所示。
我们看到它是一个动态代理对象,其中wrap方法为我们生成这个对象的动态代理对 象。
我们再看invoke方法,如果你使用这个类为插件生成代理对象,那么代理对象在调用 方法的时候就会进入到invoke方法中。在invoke方法中,如果存在签名的拦截方法,插件 的intercept方法就会被我们在这里调用,然后就返回结果。如果不存在签名方法,那么将 直接反射调度我们要执行的方法。
我们创建一个Invocation对象,其构造方法的参数包括被代理的对象、方法及其参数。 Invocation对象进行初始化,它有一个proceed()方法,如代码清单7.7所示。
代码清单7・7:反射调用被代理对象的proceed。
public Object proceed() throws InvocationTargetException, IllegalAccess Exception ( 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插件中 它是一个十分常用的工具类。
它有3个方法常常被我们用到。
- MetaObject fbrObject(Object object > Obj ectFactory obj ectFactory、Object Wrapper Factory objectWrapperFactory)方法用于包装对象。这个方法我们已经不再使用了, 而是用 MyBatis 为我们提供的 SystemMetaObject.forObject(Object obj)。
- Object getValue(String name)方法用于获取对象属性值,支持OGNL。
- void setValue(String name> Object value)方法用于修改对象属性值,支持 OGNL。
在MyBatis对象中大量使用了这个类进行包装,包括四大对象,使得我们可以通过它 来给四大对象的某些属性赋值从而满足我们的需要。
例如,拦截StatementHandler对象,我们需要先获取它要执行的SQL修改它的一些值。 这时候我们可以使用MetaObject,它为我们提供了如代码清单7-8所示的方法。
从第6章可以知道我们拦截的StatementHandler实际是RoutingStatementHandler对象,它的delegate属性才是真实服务的StatementHandler,真实的StatementHandler有一个属性BoundSqL它下面又有一个属性sql。所以才有了路径delegate.boundSql.sql。我们就可以通过这个路径去获取或者修改对应运行时的SQL。通过这样的改写,就可以限制所有查询的SQL都只能至多返回1000行记录。
由此可见,我们必须掌握好622节关于映射器解析的内容,才能准确的在插件中使用 这个类,来获取或改变MyBatis内部对象的一些重要的属性值,这对编写插件是非常重要的。
五、插件开发过程和实例
有了对插件的理解,我们再学习插件的运用就简单多了。例如,开发一个互联网项目 需要去限制每一条SQL返回数据的行数。限制的行数需要是个可配置的参数,业务可以根 据自己的需要去配置。这样很有必要,因为大型互联网系统一旦同时传输大量数据很容易 宕机。这里我们可以通过修改SQL来完成它。
1、确定需要拦截的签名
正如MyBatis插件可以拦截四大对象中的任意一个一样。从Plugin源码中我们可以看 到它需要注册签名才能够运行插件。签名需要确定一些要素。
(1)确定需要拦截的对象
首先要根据功能来确定你需要拦截什么对象。
- Executor是执行SQL的全过程,包括组装参数,组装结果集返回和执行SQL过程, 都可以拦截,较为广泛,我们一般用的不算太多。
- StatementHandler是执行SQL的过程,我们可以重写执行SQL的过程。这是我们最常用的拦截对象。
- ParameterHandler,很明显它主要是拦截执行SQL的参数组装,你可以重写组装参 数规则。
- ResultSetHandler用于拦截执行结果的组装,你可以重写组装结果的规则。
我们清楚需要拦截的是StatementHandler对象,应该在预编译SQL之前,修改SQL使得结果返回数量被限制。
(2)拦截方法和参数
当你确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在你理解了 MyBatis四大对象运作的基础上才能确定的。查询的过程是通过Executor调度StatementHandler来完成的。调度StatementHandler的 prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在此之前完成SQL 的重新编写。让我们先看看StatementHandler接口的定义,如代码清单7-9所示。
以上的任何方法都可以拦截。从接口定义而言,prepare方法有一个参数Connection对 象,因此我们按代码清单7.10的方法来设计拦截器。
其中,@Intercepts说明它是一个拦截器。©Signature是注册拦截器签名的地方,只有 签名满足条件才能拦截,type可以是四大对象中的一个,这里是StatementHandlero method 代表要拦截四大对象的某一种接口方法,而args则表示该方法的参数,你需要根据拦截对 象的方法参数进行设置。
2、实现拦截方法
这里说原理不如学习代码来得清晰明了,有了上面的原理分析,我们来看一个最简单 的插件实现方法,如代码清单7.11所示,注意看代码注解你就很明白了。
这就是一个最简单的插件,实现了一些简单的打印顺序功能,告诉大家一些常用的方 法和含义。
3、配置和运行
我们需要在MyBatis配置文件里面配置才能够使用插件,如代码清单7.12所示。请注 意plugins元素的配置顺序,你配错了顺序系统就会报错,让我们学习它。
显然,我们需要清楚配置的哪个类是插件。它会去解析注解,知道拦截哪个对象、方 法和方法的参数,在初始化的时候就会调用setProperties方法,初始化参数。
让我们运行一个插入数据的操作,看看日志打印了什么。
这里我们可以清晰地看到MyBatis调度插件的顺序。
4、插件实例
有了上面的知识来实现一个真实的插件就容易多了。在一个大型的互联网系统,我们使用的是MySQL数据库,对数据库查询返回数据量需要限制,以避免数据量过大造成网站瓶颈。假设这个数据量可以配置,当前要配置50条数据。让我们讨论一下它的实现。
首先我们先确定需要拦截四大对象中的哪一个,根据功能我们需要修改SQL的执行。SqlSession运行原理告诉我们需要拦截的是StatementHandler对象,因为是由它的prepare方法来预编译SQL语句的,我们可以在预编译前修改语句来满足我们的需求。所以我们选 择拦截StatementHandler的prepare()方法,在它预编译前,需要重写SQL,以达到要求的 结果。它有一个参数(Connection connection),所以我们就很轻易地得到了签名注解,其实现方法如代码清单7.13所示。
在setProperties方法中可以读入配置给插件的参数,一个是数据库的名称,另外一个是限制的记录数。从初始化代码可知,它在MyBaits初始化的时候就已经被设置进去了, 在需要的时候我们可以直接使用它。
在plugin方法里,我们使用了 MyBatis提供的类来生成代理对象。那么插件就会进入 plugin的invoke方法,它最后会使用到拦截器的intercept方法。
这个插件的intercept方法就会覆盖掉StatementHandler的prepare方法,我们先从代理 对象分离出真实对象,然后根据需要修改SQL,来达到限制返回行数的需求。最后使用 invocation.proceed()来调度真实StatementHandler的prepare方法完成SQL预编译,最后需 要在MyBatis配置文件里面配置才能运行这个插件,如代码清单7-14所示。
配置log4j 日志(具体请看第2章),运行一个查询语句,可以得到下面的日志信息。
在通过反射调度prepare。方法之前,SQL被我们的插件重写了,所以无论什么查询都 只可能返回至多50条数据,这样就可以限制一条语句的返回记录数,插件运行成功。
六、总结
在结束本章前,请大家注意以下6点。
- 能不用插件尽量不要用插件,因为它将修改MyBatis的底层设计。
- 插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,所以减 少插件就能减少代理,从而提高系统的性能。
- 编写插件需要了解MyBatis的运行原理,了解四大对象及其方法的作用,准确判断 需要拦截什么对象,什么方法,参数是什么,才能确定签名如何编写。
- 在插件中往往需要读取和修改MyBatis映射器中的对象属性,你需要熟练掌握6.2.2 节关于MyBatis映射器内部组成的知识。
- 插件的代码编写要考虑全面,特别是多个插件层层代理的时候,要保证逻辑的正确性。
- 尽量少改动MyBatis底层的东西,以减少错误的发生。