Mybatis_第⼋部分:Mybatis插件
8.1 插件简介
⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯作。以MyBatis
为例,我们可基于
MyBati s
插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能
8.2 Mybatis插件介绍
Mybati s
作为⼀个应⽤⼴泛的优秀的
ORM
开源框架,这个框架具有强⼤的灵活性,在四⼤组件
(Executor
、
StatementHandler
、
ParameterHandler
、
ResultSetHandler)
处提供了简单易⽤的插 件扩展机制。Mybatis
对持久层的操作就是借助于四⼤核⼼对象。
MyBatis
⽀持⽤插件对四⼤核⼼对象进 ⾏拦截,对mybatis
来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis
中的四⼤对象都是代理对象

MyBatis
所允许拦截的⽅法如下:
- 执⾏器Executor (update、query、commit、rollback等⽅法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);
8.3 Mybatis插件原理
在四⼤对象创建的时候
- 1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
- 2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包装后的对象
- 3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
拦截
插件具体是如何拦截并附加额外的功能的呢?以
ParameterHandler
来说
public ParameterHandler newParameterHandler ( MappedStatement mappedStatement ,Object object , BoundSql sql , InterceptorChain interceptorChain ){ParameterHandler parameterHandler =mappedStatement . getLang (). createParameterHandler ( mappedStatement , object , sql );parameterHandler = ( ParameterHandler )interceptorChain . pluginAll ( parameterHandler );return parameterHandler ;}public Object pluginAll ( Object target ) {for ( Interceptor interceptor : interceptors ) {target = interceptor . plugin ( target );}return target ;}
interceptorChain
保存了所有的拦截器
(interceptors)
,是
mybatis
初始化的时候创建的。调⽤拦截器链中的拦截器依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)
中的
target
就可以理解为
mybatis中的四⼤对象。返回的target
是被重重代理后的对象
如果我们想要拦截
Executor
的
query
⽅法,那么可以这样定义插件:
@Intercepts ({@Signature (type = Executor . class ,method = "query" ,args ={ MappedStatement . class , Object . class , RowBounds . class , ResultHandler . class })})public class ExeunplePlugin implements Interceptor {// 省略逻辑}
除此之外,我们还需将插件配置到
sqlMapConfig.xm l
中。
<plugins><plugin interceptor = "com.lagou.plugin.ExamplePlugin" ></plugin></plugins>
这样
MyBatis
在启动时可以加载插件,并保存插件实例到相关对象
(InterceptorChain
,拦截器链
)
中。 待准备⼯作做完后,MyBatis
处于就绪状态。我们在执⾏
SQL
时,需要先通过
DefaultSqlSessionFactory创建 SqlSession
。
Executor
实例会在创建
SqlSession
的过程中被创建,
Executor
实例创建完毕后,MyBatis会通过
JDK
动态代理为实例⽣成代理类。这样,插件逻辑即可在
Executor
相关⽅法被调⽤前执⾏。
以上就是
MyBatis
插件机制的基本原理
8.4 ⾃定义插件
8.4.1 插件接⼝
Mybatis
插件接⼝
-Interceptor
• Intercept
⽅法,插件的核⼼⽅法
• plugin
⽅法,⽣成
target
的代理对象
• setProperties
⽅法,传递插件所需参数
8.4.2⾃定义插件
设计实现⼀个⾃定义插件
Intercepts ({ // 注意看这个⼤花括号,也就这说这⾥可以定义多个 @Signature 对多个地⽅拦截,都⽤这个拦截器@Signature ( type = StatementHandler . class , // 这是指拦截哪个接⼝method = "prepare" , // 这个接⼝内的哪个⽅法名,不要拼错了args = { Connection . class , Integer . class }), 这是拦截的⽅法的⼊参,按顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的})public class MyPlugin implements Interceptor {private final Logger logger = LoggerFactory . getLogger ( this . getClass ());// // 这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内Overridepublic Object intercept ( Invocation invocation ) throws Throwable {// 增强逻辑System . out . println ( " 对⽅法进⾏了增强 ...." ) ;return invocation . proceed (); // 执⾏原⽅法}/*** // 主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中* ^Description 包装⽬标对象 为⽬标对象创建代理对象* @Param target 为要拦截的对象* @Return 代理对象*/Overridepublic Object plugin ( Object target ) {System . out . println ( " 将要包装的⽬标对象: " + target );return Plugin . wrap ( target , this );}/** 获取配置⽂件的属性 **/// 插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来Overridepublic void setProperties ( Properties properties ) {System . out . println ( " 插件配置的初始化参数: " + properties );}}
sqlMapConfig.xml
<plugins><plugin interceptor = "com.lagou.plugin.MySqlPagingPlugin" ><!-- 配置参数 --><property name = "name" value = "Bob" /></plugin></plugins>
mapper
接⼝
public interface UserMapper {List < User > selectUser ();}
mapper.xml
<mapper namespace = "com.lagou.mapper.UserMapper" ><select id = "selectUser" resultType = "com.lagou.pojo.User" >SELECTid,usernameFROMuser</select></mapper>
测试类
public class PluginTest {@Testpublic void test () throws IOException {InputStream resourceAsStream =Resources . getResourceAsStream ( "sqlMapConfig.xml" );SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder (). build ( resourceAsStream );SqlSession sqlSession = sqlSessionFactory . openSession ();UserMapper userMapper = sqlSession . getMapper ( UserMapper . class );List < User > byPaging = userMapper . selectUser ();for ( User user : byPaging ) {System . out . println ( user );}}}
8.5 源码分析
执⾏插件逻辑
Plugin
实现了
InvocationHandler
接⼝,因此它的
invoke
⽅法会拦截所有的⽅法调⽤。
invoke
⽅法会 对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:
public class PluginTest {@Testpublic void test () throws IOException {InputStream resourceAsStream =Resources . getResourceAsStream ( "sqlMapConfig.xml" );SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder (). build ( resourceAsStream );SqlSession sqlSession = sqlSessionFactory . openSession ();UserMapper userMapper = sqlSession . getMapper ( UserMapper . class );List < User > byPaging = userMapper . selectUser ();for ( User user : byPaging ) {System . out . println ( user );}}}// -Pluginpublic Object invoke ( Object proxy , Method method , Object [] args ) throwsThrowable {try {/** 获取被拦截⽅法列表,⽐如:* signatureMap.get(Executor.class), 可能返回 [query, update,commit]*/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 ){}}
invoke
⽅法的代码⽐较少,逻辑不难理解。⾸先
,invoke
⽅法会检测被拦截⽅法是否配置在插件的
@Signature
注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在
intercept
中,该⽅法的参数类型为Invocationo Invocation
主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看⼀下该类的定义
public class Invocation {private final Object target ;private final Method method ;private final Object [] args ;public Invocation ( Object targetf Method method , Object [] args ) {this . target = target ;this . method = method ;// 省略部分代码public Object proceed () throws InvocationTargetException ,IllegalAccessException { // 调⽤被拦截的⽅法>> —
关于插件的执⾏逻辑就分析结束
8.6 pageHelper分⻚插件
MyBati s
可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿
PageHelper
是将分⻚的复杂操作进⾏封装,使⽤简单的⽅式即可获得分⻚的相关数据
开发步骤:
①
导⼊通⽤
PageHelper
的坐标
②
在
mybatis
核⼼配置⽂件中配置
PageHelper
插件
③
测试分⻚数据获取
①导⼊通⽤
PageHelper
坐标
<dependency><groupId> com.github.pagehelper </groupId><artifactId> pagehelper </artifactId><version> 3.7.5 </version></dependency><dependency><groupId> com.github.jsqlparser </groupId><artifactId> jsqlparser </artifactId><version> 0.9.1 </version></dependency>
②
在
mybatis
核⼼配置⽂件中配置
PageHelper
插件
<!-- 注意:分⻚助⼿的插件 配置在通⽤馆 mapper 之前 *--> *<plugin interceptor = "com.github.pagehelper.PageHelper" ><!— 指定⽅⾔ —><property name = "dialect" value = "mysql" /></plugin>
③
测试分⻚代码实现
@Testpublic void testPageHelper () {// 设置分⻚参数PageHelper . startPage ( 1 , 2 );List < User > select = userMapper2 . select ( null );for ( User user : select ) {System . out . println ( user );}}}
获得分⻚相关的其他参数
// 其他分⻚的数据PageInfo < User > pageInfo = new PageInfo < User > ( select );System . out . println ( " 总条数: " + pageInfo . getTotal ());System . out . println ( " 总⻚数: " + pageInfo . getPages ());System . out . println ( " 当前⻚: " + pageInfo . getPageNum ());System . out . println ( " 每⻚显万⻓度: " + pageInfo . getPageSize ());System . out . println ( " 是否第⼀⻚: " + pageInfo . isIsFirstPage ());System . out . println ( " 是否最后⼀⻚: " + pageInfo . isIsLastPage ());
8.7 通⽤ mapper
什么是通⽤
Mapper
通⽤
Mapper
就是为了解决单表增删改查,基于
Mybatis
的插件机制。开发⼈员不需要编写
SQL,
不需要在DAO
中增加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法
如何使⽤
1. ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖
<dependency><groupId> tk.mybatis </groupId><artifactId> mapper </artifactId><version> 3.1.2 </version></dependency>
2. Mybatis配置⽂件中完成配置
<plugins><!-- 分⻚插件:如果有分⻚插件,要排在通⽤ mapper 之前 --><plugin interceptor = "com.github.pagehelper.PageHelper" ><property name = "dialect" value = "mysql" /></plugin><plugin interceptor = "tk.mybatis.mapper.mapperhelper.MapperInterceptor" ><!-- 通⽤ Mapper 接⼝,多个通⽤接⼝⽤逗号隔开 --><property name = "mappers" value = "tk.mybatis.mapper.common.Mapper" /></plugin></plugins>
3. 实体类设置主键
@Table ( name = "t_user" )public class User {@Id@GeneratedValue ( strategy = GenerationType . IDENTITY )private Integer id ;private String username ;}
4. 定义通⽤mapper
import com . lagou . domain . User ;import tk . mybatis . mapper . common . Mapper ;public interface UserMapper extends Mapper < User > {}
5. 测试
public class UserTest {@Testpublic void test1 () throws IOException {Inputstream resourceAsStream =Resources . getResourceAsStream ( "sqlMapConfig.xml" );SqlSessionFactory build = newSqlSessionFactoryBuilder (). build ( resourceAsStream );SqlSession sqlSession = build . openSession ();UserMapper userMapper = sqlSession . getMapper ( UserMapper . class ); User user = new User ();user . setId ( 4 );//(1)mapper 基础接⼝//select 接⼝User user1 = userMapper . selectOne ( user ); // 根据实体中的属性进⾏查询,只能有— 个返回值List < User > users = userMapper . select ( null ); // 查询全部结果userMapper . selectByPrimaryKey ( 1 ); // 根据主键字段进⾏查询,⽅法参数必须包含完整的主键属性,查询条件使⽤等号userMapper . selectCount ( user ); // 根据实体中的属性查询总数,查询条件使⽤等号// insert 接⼝int insert = userMapper . insert ( user ); // 保存⼀个实体, null 值也会保存,不会使⽤数据库默认值int i = userMapper . insertSelective ( user ); // 保存实体, null 的属性不会保存,会使⽤数据库默认值// update 接⼝int i1 = userMapper . updateByPrimaryKey ( user ); // 根据主键更新实体全部字段,null 值会被更新// delete 接⼝int delete = userMapper . delete ( user ); // 根据实体属性作为条件进⾏删除,查询条件 使⽤等号userMapper . deleteByPrimaryKey ( 1 ); // 根据主键字段进⾏删除,⽅法参数必须包含完整的主键属性//(2)example ⽅法Example example = new Example ( User . class );example . createCriteria (). andEqualTo ( "id" , 1 );example . createCriteria (). andLike ( "val" , "1" );// ⾃定义查询List < User > users1 = userMapper . selectByExample ( example );}}