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

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

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

 


 

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

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

(1)确定拦截对象

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

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

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

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

(2)拦截方法和参数

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

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

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

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

@Intercepts说明是一个拦截器;

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

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

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

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

插件初始化是在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方法设置参数,然后将插件实例保存到配置对象中,以便读取使用它。所以插件实例对象是一开始就被初始化的,而不是用到的时候才初始化。

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

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

pluginAll()方法的实现:

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

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

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

它可以有效地读取或者修改一些重要对象的属性,在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. while (metaStatementHandler.hasGetter("h")) {
  6. Object object = metaStatementHandler.getValue("h");
  7. metaStatementHandler = SystemMetaObject.forObject(object);
  8. }
  9. // 分离最后一个代理对象的目标类
  10. while (metaStatementHandler.hasGetter("target")) {
  11. Object object = metaStatementHandler.getValue("target");
  12. metaStatementHandler = SystemMetaObject.forObject(object);
  13. }
  14. // 取出即将执行的SQL
  15. String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
  16. String limitSql;
  17. // 判断是否是MySQL数据库且SQL没有被重写过
  18. if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
  19. sql = sql.trim();
  20. // 将参数写入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
  21. limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
  22. // 重写要执行的SQL
  23. metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
  24. }

 实际开发过程中我可能需要限制每次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)拦截方法与参数:拦截预编译,自然是要拦截StatementHandlerprepare()方法,prepare()方法传入参数Connection对象。最后设计拦截器签名如下:

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

(3)实现拦截方法:

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

(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

 

版权声明:本文为jian0110原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/jian0110/p/9510957.html