前言

  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)方法。

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException; void parameterize(Statement statement)
throws SQLException; void batch(Statement statement)
throws SQLException; int update(Statement statement)
throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException; <E> Cursor<E> queryCursor(Statement statement)
throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }

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

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

@Intercepts说明是一个拦截器;

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

2、插件接口

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

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

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

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

3、插件初始化

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

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

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

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

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

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

pluginAll()方法的实现:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
// 从interceptors中取出传递给plugin()方法,返回一个代理target
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// interceptor实例存于interceptors这个List中(也就是上述所说的加入到Congfiguration对象中)
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
} public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }

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。在插件下修改运行参数如下:

     // 取出被拦截对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler); // 分离代理对象,从而形成多次代理
while (metaStatementHandler.hasGetter("h")) {
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
// 分离最后一个代理对象的目标类
while (metaStatementHandler.hasGetter("target")) {
Object object = metaStatementHandler.getValue("target");
metaStatementHandler = SystemMetaObject.forObject(object);
} // 取出即将执行的SQL
String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
String limitSql; // 判断是否是MySQL数据库且SQL没有被重写过
if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
sql = sql.trim();
// 将参数写入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
// 重写要执行的SQL
metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
}

二、插件开发实例

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

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

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

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

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

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

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

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

(3)实现拦截方法:

//拦截StatementHandler对象的prepare预处理方法,同时指定该该方法的Connection参数
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class QueryLimitPlugin implements Interceptor{ // 默认限制查询返回行数
private int limit;
// 数据库类型
private String dbType;
// 为了防止表名不冲突,起一个特殊的中间表名
private static final String LIMIT_TABLE_NAME = "limit_table_name_1"; @Override
public Object intercept(Invocation invocation) throws Throwable {
// 取出被拦截对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler); // 分离代理对象,从而形成多次代理
while (metaStatementHandler.hasGetter("h")) {
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
// 分离最后一个代理对象的目标类
while (metaStatementHandler.hasGetter("target")) {
Object object = metaStatementHandler.getValue("target");
metaStatementHandler = SystemMetaObject.forObject(object);
} // 取出即将执行的SQL
String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
String limitSql; // 判断是否是MySQL数据库且SQL没有被重写过
if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
sql = sql.trim();
// 将参数写入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
// 重写要执行的SQL
metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
}
// 调用原对象的方法,进入责任链的下一层
return invocation.proceed();
} @Override
public Object plugin(Object target) {
// 使用默认的Mybatis提供的类生成代理对象
return Plugin.wrap(target, this);
} @Override
public void setProperties(Properties properties) {
// 读取设置的limit
String strLimit = properties.getProperty("limit","4");
this.limit = Integer.parseInt(strLimit);
// 读取设置的数据库类型
this.dbType = (String)properties.getProperty("dbType", "mysql");
} }

(4)配置与运行:

在mybatis-config.xml中:

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

在userMapper.xml配置<select>

<select id="listUsers" resultMap="userMap">
select * from test_table
</select>

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

List<User> listUsers();

测试类:

public class MybatisMain2 {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
//获得SqlSession
sqlSession = SqlSessionFactoryUtils.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users= userMapper.listUsers();
users.forEach(user -> {
System.out.println(user.toString());
});
} catch (Exception e) {
System.err.println(e.getMessage());
}
finally {
if (sqlSession != null) {
//sqlSession生命周期是随着SQL查询而结束的
sqlSession.close();
}
}
}
}

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

[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.0
[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
[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters:
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Columns: id, name, gender
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 1, Lijian, M
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 2, Zhangtao, F
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 3, Zhangsan, M
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 4, Lisi, M
[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. Python连接oracle数据库的基本操作

    1,创建数据库连接connect和关闭数据库连接close 1.1 创建数据库连接的三种方式: 方法一:用户名.密码和监听分开写 import cx_Oracle db=cx_Oracle.conne ...

  2. javaean(web作业)

    javabean简介: javabean是java语言开发的可重用组件,在jsp中使用javabean可以减少代码的重复量,使代码简洁. 他的优点: 1.可将HTML和Java代码分离,这主要是为了日 ...

  3. ES6学习:两个面试题目--关于模板字符串

    号称看完就能“让开发飞起来”,不过文中的两个面试题目的知识点并没包括在文中. https://www.jianshu.com/p/287e0bb867ae 文中并没有完整的知识点去完成上面的两道题,这 ...

  4. android-audioRecord

    android 录音功能 录音的大致流程,流程图可以在文件下载:mediarecord.vsdx 切换设备.谁去更新播放流,自动选择新的设备?流程?

  5. JDBC数据库

    JDBC是Java程序连接和存取数据库的应用程序接口(API),包括两个包:java.sql和javax.sql. 用JDBC访问数据库的一般步骤: 1.建立数据源 2.装入JDBC驱动程序:使用Cl ...

  6. Runtime "Apache Tomcat v6.0 (3)" is invalid. The JRE could not be found. Edit the server and change the JRE location解决方案

    使用eclipse,启动Tomcat时出现The JRE could not be found ,Edit server and change teh JRE location的错误提示! 原因:重装 ...

  7. 网站模仿——LOFTER个人主页

    代码片段 码云链接:https://gitee.com/gulveig/codes/e7q95kub6clvxt2ydi84g83

  8. Android中弹出dialog后无法捕捉back键

    一.需求 在Android开发过程中,弹出dialog后无法捕捉back键,点击back按键无响应. 二.解决方案 原因:弹出dialog后,activity失去焦点,dialog获得当前焦点. 解决 ...

  9. repo 用法

    repo的用法(zz) 注:repo只是google用Python脚本写的调用git的一个脚本,主要是用来下载.管理Android项目的软件仓库.(也就是说,他是用来管理给git管理的一个个仓库的) ...

  10. 从NoSQL到NewSQL,谈交易型分布式数据库建设要点

    在上一篇文章<从架构特点到功能缺陷,重新认识分析型分布式数据库>中,我们完成了对不同"分布式数据库"的横向分析,本文Ivan将讲述拆解的第二部分,会结合NoSQL与Ne ...