前言

  Mybatis的插件开发过程的前提是必须要对Mybatis整个SQL执行过程十分熟悉,这样才能正确覆盖源码保证插件运行,总的来说Mybatis的插件式一种侵入式插件,使用时应该十分注意。

  在之前我的博文中已经介绍Mybatis的SqlSession运行原理,本篇博文是在此知识基础上学习记录的,读者可以先回顾再来看本博文。

  主要参数资料《深入浅出Myabtis基础原理与实现》(PDF高清电子版,有需要的朋友可以评论/私信我)


一、插件开发前准备

插件开发前,我们需要知道签名、插件接口、插件如何初始化、插件代理与反射、分离拦截对象常用工具类等

1、确定签名

插件开发前,需要确定我们拦截的签名,而签名的确定需要以下的两个因素

(1)确定拦截对象

Executor:调度以下三个对象并且执行SQL全过程,组装参数、执行SQL、组装结果集返回。通常不怎么拦截使用。

StatementHandler:是执行SQL的过程(预处理语句构成),这里我们可以获得SQL,重写SQL执行。所以这是最常被拦截的对象。

ParameterHandler:参数组装,可以拦截参数重组参数。

ResultSetHandler:结果集处理,可以重写组装结果集返回。

(2)拦截方法和参数

确定了拦截对象之后,需要确定拦截对象的方法与参数,比如拦截的是StatementHandler对象的关键预处理prepare(Connection connection, Integer transactionTimeout)方法。

  1. public interface StatementHandler {
  2.  
  3. Statement prepare(Connection connection, Integer transactionTimeout)
  4. throws SQLException;
  5.  
  6. void parameterize(Statement statement)
  7. throws SQLException;
  8.  
  9. void batch(Statement statement)
  10. throws SQLException;
  11.  
  12. int update(Statement statement)
  13. throws SQLException;
  14.  
  15. <E> List<E> query(Statement statement, ResultHandler resultHandler)
  16. throws SQLException;
  17.  
  18. <E> Cursor<E> queryCursor(Statement statement)
  19. throws SQLException;
  20.  
  21. BoundSql getBoundSql();
  22.  
  23. ParameterHandler getParameterHandler();
  24.  
  25. }

因此我们可以定义这样的签名:

  1. //拦截StatementHandler对象的prepare预处理方法,同时指定该该方法的Connection参数
  2. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

@Intercepts说明是一个拦截器;

@Signature是注册拦截器签名的地方,只有满足签名条件才能拦截,type是四大对象中的一个。Method是指拦截的方法,args表示该方法参数。

2、插件接口

插件的开发第一步必须先实现Interceptor插件接口:

  1. public interface Interceptor {
  2.  
  3. Object intercept(Invocation invocation) throws Throwable;
  4.  
  5. Object plugin(Object target);
  6.  
  7. void setProperties(Properties properties);
  8.  
  9. }
  • intercept方法:它是直接覆盖你所拦截对象原有方法,因此它是插件的核心方法。intercept里面有个参数Invoction(Invocation.getTarget()方法获得拦截队对象),通过它调用真正的对象方法(动态代理中经常使用)
  • plugin方法:target是被拦截对象,它的作用是给拦截对象生成一个代理对象,并返回它(使用Plugin.wrap(target,this)方法)。当然也可以自己实现,但是需要特别小心。它实现InvoctionHandler接口,采用JDK动态代理。
  • setProperties方法:允许在mybatis-config.xml配置文件plugin元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后续获取。

以上其实就是模板(template)模式提供一个骨架,并告知骨架中的方法是用来做什么的。

3、插件初始化

插件初始化是在Mybatis初始化的时候完成,我们可以通过XMLConfigBuilder中的代码便可以知道。

  1. private void pluginElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. String interceptor = child.getStringAttribute("interceptor");
  5. Properties properties = child.getChildrenAsProperties();
  6. // 通过反射生成插件实例
  7. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  8. // 配置参数
  9. interceptorInstance.setProperties(properties);
  10. // 保存到配置对象中
  11. configuration.addInterceptor(interceptorInstance);
  12. }
  13. }
  14. }

在Mybatis上下文初始化过程中,就开始读入插件节点和我们配置的参数,同时使用反射技术生成对应插件实例,然后调用插件方法中的setProperties方法设置参数,然后将插件实例保存到配置对象中,以便读取使用它。所以插件实例对象是一开始就被初始化的,而不是用到的时候才初始化。

4、插件的代理与反射设计

插件使用的是责任链模式(每一个在责任链上的角色都有机会去处理拦截对象),Mybatis中责任链是interceptorChain定义,比如执行器的生成

  1. executor = (Executor)interceptorChain.pluginAll(executor);

pluginAll()方法的实现:

  1. public class InterceptorChain {
  2.  
  3. private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  4. // 从interceptors中取出传递给plugin()方法,返回一个代理target
  5. public Object pluginAll(Object target) {
  6. for (Interceptor interceptor : interceptors) {
  7. target = interceptor.plugin(target);
  8. }
  9. return target;
  10. }
  11. // interceptor实例存于interceptors这个List中(也就是上述所说的加入到Congfiguration对象中)
  12. public void addInterceptor(Interceptor interceptor) {
  13. interceptors.add(interceptor);
  14. }
  15.  
  16. public List<Interceptor> getInterceptors() {
  17. return Collections.unmodifiableList(interceptors);
  18. }
  19.  
  20. }

pluginAll(Object target)方法:从Configuration对象中取出的。从第一个对象到第四个对象(上述介绍过的四大对象)一次传递给plugin方法,然后返回一个代理target。如果存在第二个插件,那么就拿到第一个代理对象,传递给plugin方法再返回第一个代理对象的代理.......依次类推。总之有多少个拦截器就有多少个代理对象。

addInterceptor(Interceptor interceptor)方法:将我们自定义的实现插件接口的interceptor实例存于interceptors这个List中(也就是上述所说的加入到Congfiguration对象中)

5、常用工具类MetaObject

它可以有效地读取或者修改一些重要对象的属性,在Mybatis中的四大对象提供的public设置参数方法很少,很难获得相应的属性,但是通过MetaObject工具类就可以读取或修改这些属性。常用的有三个方法:

  (1)MetaObject forObject(...)方法用来包装对象,但是目前来说已经不再使用,而是使用SystemMetaObject.forObject(Object object)

  (2)Object getValue(String name)方法获取对象属性值,支持OGNL

  (3)void setValue(String name, Object value)方法修改对象属性值,支持OGNL

Mybatis对象中大量使用这个类进行包装,包括四大对象,使得我们可以通过它来给四大对象的某些属性赋值从而满足要求。

比如,拦截StatementHandler对象,我们先获取要执行SQL修改它的值,这时候就使用MetaObject。在插件下修改运行参数如下:

  1.      // 取出被拦截对象
  2. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  3. MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
  4.  
  5. // 分离代理对象,从而形成多次代理
  6. while (metaStatementHandler.hasGetter("h")) {
  7. Object object = metaStatementHandler.getValue("h");
  8. metaStatementHandler = SystemMetaObject.forObject(object);
  9. }
  10. // 分离最后一个代理对象的目标类
  11. while (metaStatementHandler.hasGetter("target")) {
  12. Object object = metaStatementHandler.getValue("target");
  13. metaStatementHandler = SystemMetaObject.forObject(object);
  14. }
  15.  
  16. // 取出即将执行的SQL
  17. String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
  18. String limitSql;
  19.  
  20. // 判断是否是MySQL数据库且SQL没有被重写过
  21. if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
  22. sql = sql.trim();
  23. // 将参数写入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
  24. limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
  25. // 重写要执行的SQL
  26. metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
  27. }

二、插件开发实例

实际开发过程中我可能需要限制每次SQL返回的数据行数,限制的行数需要是一个可配置的参数,也去可以根据自己的需要配置。有如下的数据表,假设每次我只需要返回4条数据记录!

注:以下的源代码都是使用Mybatis的SqlSession运行原理中的代码,有需要源码的可以下方评论!

  1. mysql> select * from test_table;
  2. +----+----------+--------+
  3. | id | name | gender |
  4. +----+----------+--------+
  5. | 1 | Lijian | M |
  6. | 2 | Zhangtao | F |
  7. | 3 | Zhangsan | M |
  8. | 4 | Lisi | M |
  9. | 5 | Wangwu | M |
  10. | 6 | Zhaoliu | F |
  11. | 7 | Zhouqi | F |
  12. | 8 | test | M |
  13. +----+----------+--------+
  14. 8 rows in set

那么,可以通过以下简单几步实现插件实现(SQL拦截)

(1)确定需要拦截对象:限制返回条数肯定是先要拦截StatementHandler对象,在预编译SQL之前,修改SQL返回数量。

  1. # Mapper中原始的SQL
  2. select * from test_table
  3. # 我们最后需要的SQL,也就插件最后执行的SQL
  4. select * from (select * from test_table) temp_table_nmae limit 4

(2)拦截方法与参数:拦截预编译,自然是要拦截StatementHandler的prepare()方法,prepare()方法传入参数Connection对象与超时参数Integer类型。最后设计拦截器签名如下:

  1. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

(3)实现拦截方法:

  1. //拦截StatementHandler对象的prepare预处理方法,同时指定该该方法的Connection参数
  2. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
  3. public class QueryLimitPlugin implements Interceptor{
  4.  
  5. // 默认限制查询返回行数
  6. private int limit;
  7. // 数据库类型
  8. private String dbType;
  9. // 为了防止表名不冲突,起一个特殊的中间表名
  10. private static final String LIMIT_TABLE_NAME = "limit_table_name_1";
  11.  
  12. @Override
  13. public Object intercept(Invocation invocation) throws Throwable {
  14. // 取出被拦截对象
  15. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  16. MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
  17.  
  18. // 分离代理对象,从而形成多次代理
  19. while (metaStatementHandler.hasGetter("h")) {
  20. Object object = metaStatementHandler.getValue("h");
  21. metaStatementHandler = SystemMetaObject.forObject(object);
  22. }
  23. // 分离最后一个代理对象的目标类
  24. while (metaStatementHandler.hasGetter("target")) {
  25. Object object = metaStatementHandler.getValue("target");
  26. metaStatementHandler = SystemMetaObject.forObject(object);
  27. }
  28.  
  29. // 取出即将执行的SQL
  30. String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
  31. String limitSql;
  32.  
  33. // 判断是否是MySQL数据库且SQL没有被重写过
  34. if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
  35. sql = sql.trim();
  36. // 将参数写入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
  37. limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
  38. // 重写要执行的SQL
  39. metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
  40. }
  41. // 调用原对象的方法,进入责任链的下一层
  42. return invocation.proceed();
  43. }
  44.  
  45. @Override
  46. public Object plugin(Object target) {
  47. // 使用默认的Mybatis提供的类生成代理对象
  48. return Plugin.wrap(target, this);
  49. }
  50.  
  51. @Override
  52. public void setProperties(Properties properties) {
  53. // 读取设置的limit
  54. String strLimit = properties.getProperty("limit","4");
  55. this.limit = Integer.parseInt(strLimit);
  56. // 读取设置的数据库类型
  57. this.dbType = (String)properties.getProperty("dbType", "mysql");
  58. }
  59.  
  60. }

(4)配置与运行:

在mybatis-config.xml中:

  1. <!-- 插件配置 -->
  2. <plugins>
  3. <plugin interceptor="com.lijian.mybatis.plugin.QueryLimitPlugin">
  4. <property name="dbType" value="mysql"/>
  5. <property name="limit" value="4"/>
  6. </plugin>
  7. </plugins>

在userMapper.xml配置<select>

  1. <select id="listUsers" resultMap="userMap">
  2. select * from test_table
  3. </select>

在UserMapper.java接口中编写listUsers方法:

  1. List<User> listUsers();

测试类:

  1. public class MybatisMain2 {
  2. public static void main(String[] args) {
  3. SqlSession sqlSession = null;
  4. try {
  5. //获得SqlSession
  6. sqlSession = SqlSessionFactoryUtils.openSession();
  7. UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  8. List<User> users= userMapper.listUsers();
  9. users.forEach(user -> {
  10. System.out.println(user.toString());
  11. });
  12. } catch (Exception e) {
  13. System.err.println(e.getMessage());
  14. }
  15. finally {
  16. if (sqlSession != null) {
  17. //sqlSession生命周期是随着SQL查询而结束的
  18. sqlSession.close();
  19. }
  20. }
  21. }
  22. }

查看日志打印结果:发现我们最初的SQL语句select  *from test_table变为select * from(select*from test_table) limit_table_name_1 limit 4,表示SQL已经被拦截修改执行

  1. [DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.0
  2. [DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Preparing: select * from (select * from test_table) limit_table_name_1 limit 4
  3. [DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters:
  4. [TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Columns: id, name, gender
  5. [TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 1, Lijian, M
  6. [TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 2, Zhangtao, F
  7. [TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 3, Zhangsan, M
  8. [TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 4, Lisi, M
  9. [DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Total: 4

动手实践Mybatis插件的更多相关文章

  1. 自己动手编写一个Mybatis插件:Mybatis脱敏插件

    1. 前言 在日常开发中,身份证号.手机号.卡号.客户号等个人信息都需要进行数据脱敏.否则容易造成个人隐私泄露,客户资料泄露,给不法分子可乘之机.但是数据脱敏不是把敏感信息隐藏起来,而是看起来像真的一 ...

  2. 自己动手写Android插件化框架,让老板对你刮目相看

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由达文西发表于云+社区专栏 最近在工作中接触到了Android插件内的开发,发现自己这种技术还缺乏最基本的了解,以至于在一些基本问题上浪 ...

  3. 自己动手写Android插件化框架

    自己动手写Android插件化框架 转 http://www.imooc.com/article/details/id/252238   最近在工作中接触到了Android插件内的开发,发现自己这种技 ...

  4. 深入理解Mybatis插件

    Mybatis插件实现原理 本文如有任何纰漏.错误,请不吝指出,谢谢! 首先,我并没有使用过 Mybatis的插件,但是这个和我写这篇文章并不冲突,估计能真正使用到插件的人也比较少,写这篇文章的目的主 ...

  5. Mybatis插件,能做的事情真的很多

    大家好,我是架构摆渡人.这是实践经验系列的第九篇文章,这个系列会给大家分享很多在实际工作中有用的经验,如果有收获,还请分享给更多的朋友. Mybatis是我们经常用的一款操作数据库的框架,它的插件机制 ...

  6. 如何在IDEA上 添加GIT和maven、mybatis插件

    IDEA工具上,添加GIT和maven.mybatis插件,相对比较简单: 首先下载GIT.maven.mybatis. 先添加GIT插件: 首先在IDEA找到file中找到setting,然后搜索g ...

  7. Intelij IDEA 2016.3安装mybatis插件并激活教程

    转载自:http://blog.csdn.net/solo_talk/article/details/53540449 现在Mybatis框架越来越受欢迎,Intelij IDEA这个编辑器逐渐成为很 ...

  8. 关于使用mybatis插件自动生成代码

    1.安装 mybatis 插件: 在 eclipse 中 点击 help-->Install New Software...--> Add --> local  选择插件中eclip ...

  9. intellij IDEA mybatis插件破解方法

    1>安装mybatis插件,找到mybatis_plus.jar包的位置,在C:\Users\LZHL\.IntelliJIdea2016.3\config\plugins\mybatis_pl ...

随机推荐

  1. entity framework 上下文对象跟踪相关

    entity framework 上下文对于对象的跟踪有2中方式进行控制,第一种从数据库查询但不加载到上下文. 这里可以用到.AsNoTracing()方法. 这里用到的是实体(entity)在上下文 ...

  2. 基本数据类型的包装类(Interger)

    基本数据类型 vs包装类 byte Byte short Short char Character int Integer long Long float Float double Double bo ...

  3. 《Node.js高级编程》之Node 核心API基础

    Node 核心API基础 第三章 加载模块 第四章 应用缓冲区 第五章 事件发射器模式简化事件绑定 第六章 使用定时器制定函数执行计划 第三章 加载模块 本章提要 加载模块 创建模块 使用node_m ...

  4. Python之旅Day15 Bootstrap与Django初识

    Bootstrap初识 Bootstrap简介 Bootstrap 是最受欢迎的 HTML.CSS 和 JS 框架,用于开发响应式布局.移动设备优先的 WEB 项目.(官网http://www.boo ...

  5. Linux下MySQL数据库的安装

    记录详细过程以备使用 1.创建群组及用户 obd:~ # groupadd mysql obd:~ # useradd -g mysql mysql 2.创建相关目录 obd:~ # mkdir -p ...

  6. C#程序以管理员权限运行(ZT)

    本文转载:http://www.cnblogs.com/Interkey/p/RunAsAdmin.html 在Vista 和 Windows 7 及更新版本的操作系统,增加了 UAC(用户账户控制) ...

  7. genymotion常见问题解答

    [转]常见问题解答 很多人喜欢使用Genymotion这款安卓模拟器,但是虽然Genymotion很好用,可是却有各种问题存在哦,下面潇潇就一些常见的Genymotion问题来说下解决方法吧. 为什么 ...

  8. Git使用详细教程(5):修改提交说明

    在使用git的过程中,我们有时在提交时,注释说明会写错,那么我们该如何修改这次提交说明呢?在SVN上我们只能在代码的某个地方加一个不影响功能的空格再次提交,然后写新说明.但是在Git中我们可以吃后悔药 ...

  9. Day1:html和css

    Day1:html和css 了解浏览器 掌握WEB标准 理解标签语义 掌握常用标签 掌握三种列表标签 前端开发工具: 浏览器是网页显示.运行的平台,IE.火狐(Firefox).谷歌(Chrome). ...

  10. FF中flash滚轮失效的解决方案

    概述 在FF浏览器中有这样一个bug,就是当鼠标hover在flash区域的时候,滚轮会失效.原因是ff浏览器没有把滚轮事件嵌入到flash里面去.如果这个flash很小的话,比如直播的视频,会很容易 ...