首页 > 代码库 > 《深入浅出MyBatis技术原理与实战》——7. 插件

《深入浅出MyBatis技术原理与实战》——7. 插件

在第6章讨论了四大运行对象的运行过程,在Configuration对象的创建方法里我们看到了MyBatis用责任链去封装它们。

7.1 插件接口

在MyBatis中使用插件,我们必须使用接口Interceptor,先来看看它的定义和各个方法的含义:

技术分享

 技术分享

在接口各中,运用了3个方法,这3个方法的含义是:

技术分享

这里看到了插件的骨架,这样的模式我们称为模版模式,就是提供一个骨架,并且告知骨架中的方法是干什么用的,由开发者来完成它。

 

7.2 插件的初始化

插件的初始化实在MyBatis初始化的时候完成的,这点可以通过XMLConfigBuilder中的代码知道:

技术分享

技术分享

在解析配置文件的时候,在MyBatis的上下文初始化过程中,就开始读入插件节点和我们配置的参数,同时使用反射技术生成对应的插件实例,然后调用插件方法中的setProperties方法,设置我们配置的参数,然后将插件实例保存到配置对象中,以便读取和使用它。所以插件的实例对象是一开始就被初始化的,而不是用到的时候才初始化的,我们使用它的时候,直接拿出来就可以了,这样有助于性能的提高。

再来看看插件在Configuration对象里是怎样保存的:

技术分享

InterceptorChain在Configuration里面是一个属性,它里面有个addInterceptor方法:

技术分享

显然,完成初始化的插件就保存在这个List对象里面等待将其取出使用。

 

7.3 插件的代理和反射设计

插件用的是责任链模式。就是一个对象,在MyBatis中可能是四大对象中的一个,在多个角色中传递,处在传递链上的任何角色都有处理它的机会。

MyBatis的责任链是由InterceptorChain去定义的,MyBatis在创建执行器时用到这样的代码:

技术分享

来看看pluginAll()方法是如何实现的:

技术分享

plugin方法是生成代理对象的方法,当它取出插件的时候是从Configuration对象中去取的。从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法,然后返回一个代理,如果存在第二个插件,那么就拿到第一个代理对象的代理,传递给plugin方法再返回第一个代理对象的代理......以此类推,有多少个拦截器就生成多少个代理对象。这样有多少个拦截器就生成多少个代理对象。

MyBatis中提供了一个常用的工具类来生成代理对象,它便是Plugin类。Plugin类实现了InvocationHandler接口,采用的是JDK动态代理,我们首先看看这个类的两个十分重要的方法:

  技术分享

技术分享

看以看到它是一个动态代理对象,其中wrap方法为我们生成这个对象的动态代理对象。至于invoke方法,如果使用这个类为插件生成代理对象,那么代理对象在调用方法的时候就会进入到invoke方法中。在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会被我们在这里调用,然后就返回结果。如果不存在签名的方法,那么将直接调度我们要执行的方法。

我们创建一个Invocation对象,其构造方法的参数包括被代理对象、方法及其参数。Invocation对象进行初始化,它有一个proceed()方法:

技术分享

        技术分享

这个方法就是调度被代理对象的真实方法。现在假设有n个插件,我们知道第一个传递的参数是四大对象本身,然后调用一次wrap方法产生第一个代理对象,而这里的反射就是反射四大对象本身的真实方法。如果有第二个插件,我们会将第一个代理对象传递给wrap方法,生成第二个代理对象,这里的反射就是指第一个代理对象的invoke方法,依次类推直至最后一个代理对象。如果每一个代理对象都调用这个proceed方法,那么最后四大对象本身的方法也会被调用,只是它会从最后一个代理对象的invoke方法运行到第一个代理对象的invoke方法,直至四大对象的真实方法。

 

7.4 常用的工具类——MetaObject

可以有效的读取或者修改一些重要的对象属性。它有3个方法常常用到:

技术分享

技术分享

我们可以通过使用它来给四大对象的某些属性赋值从而满足我们的需要。例如,拦截StatementHandler对象,我们需要先获取她要执行的SQL修改它的一些值。这个时候我们可以使用MetaObject,如:

技术分享

拦截的StatementHandler实际上是RoutingStatementHandler对象,它的delegate属性才是真实服务的StatementHandler,真实的StatementHandler有一个属性BoundSql,它下面又有一个属性sql。所以才有了路径delegate.boundSql.sql。我们就可以通过这个路径获取或者修改对应运行时的SQL。

 

7.5 插件开发过程实例

限制每一条SQL返回数据的行数。限制的行数是个可配置的参数,业务可以根据自己的需要去配置。

7.5.1 确定需要拦截的签名

从Plugin源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。

(1) 确定需要拦截的签名

技术分享

那么我们需要拦截的是StatementHandler对象,应该在预编译SQL之前,修改SQL使得返回数量被限制。

(2) 拦截方法和参数

查询是通过Executor调度StatementHandler来完成的。调度StatementHandler的prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在此之前完成SQL的重新编译。先来看看那StatementHandler接口的定义:

技术分享

以上的任何方法都可以被拦截。从接口的定义而言,prepare方法有一个参数Connection对象,因此我们按如下代码来设计拦截器:

技术分享

其中@Intercepts说明它是一个拦截器。@Signature是注册拦截器签名的地方,只有签名满足条件才能被拦截,type可以使四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一种接口方法,而args则表示该方法的参数。

7.5.4 插件实例

限制查询返回的数据量。需要拦截的是StatementHandler对象

技术分享

 技术分享

技术分享

技术分享

在setProperties方法中可以读入配置给插件的参数,一个是数据库的名称,另外一个是限制的记录数。在plugin方法里,使用了MyBatis提供的类来生成代理对象。那么插件就会进入plugin的invoke方法,它最后会使用到拦截器的intercept方法。现在我们需要在MyBatis配置文件里面配置才能运行这个插件:

技术分享


注意:

技术分享

技术分享

《深入浅出MyBatis技术原理与实战》——7. 插件