前言

  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. 关于H5在微信获取授权

    很尴尬,flag倒了很久,这才来更新. 1.作为一枚小前端,所做的就是把微信获取授权之后的链接和所需的参数给到后端,定好之后只要获取链接就好了.(⊙o⊙)…确实就是这么简单,基本上这种授权是需要后端来 ...

  2. 使用tcpdump探测TCP/IP三次握手

    读计算机应该就同说过TCP/IP三次握手,但是都没有去验证过,今天心血来潮,去验证了一下,于是乎写下了这篇博客,可能写的可能有问题,还请多多指教 包括我学习,还有从很多资料来看资料,第三次握手,应该会 ...

  3. Catalog

      Java SE EE| Hibernate | Struts2Spring/SpringMVC | MyBatis C# Python PHP C/C++ | STL 汇编语言           ...

  4. Python3--Numpy

    数组的形状是它有多少行和列,上面的数组有5行和5列,所以它的形状是(5,5). itemsize属性是每个项占用的字节数.这个数组的数据类型是int 64,一个int 64中有64位,一个字节中有8位 ...

  5. 快乐python 零基础也能P图 —— PIL库

    Python PIL PIL (Python Image Library) 库是Python 语言的一个第三方库,PIL库支持图像存储.显示和处理,能够处理几乎所有格式的图片. 一.PIL库简介 1. ...

  6. IT行业三大定律

    1:摩尔定律 该定律由Inter公司创始人戈登摩尔提出,摩尔定律指出:每一年半计算机等IT产品的性能会翻一番:或者说相同性能的产品在一年半后价格会降一半.   表现为:为适应摩尔定律,IT公司必须在较 ...

  7. 《Serverless架构-无服务单页应用开发》读后感

    本书的作者是[美]Ben Rady,译者郑赞美.简传挺,书中作者详细的介绍了如何使用html.js以及amazon提供的诸多云服务(Simple Storage Service(S3).Cognito ...

  8. python insert所用 插入到自定的位置

    a = list(range(50)) b = list(range(50)) c = [] for x in a: c.insert(x, [a[x], b[x]]) print(c)

  9. Vipe-技术选型

    1.AOP,IOC框架-Spring 选择Spring是最不需要考虑的.应该90%以上的JAVA项目都有用Spring. 2.ORM框架-Mybatis Mybatis入门比较简单,并且对SQL语法的 ...

  10. 安全圈玩起了直播,"学霸”带你玩转CTF

    [i春秋]安全圈玩起了直播,"学霸”带你玩转CTF 跟着学霸(汪神)打CTF,摸清CTF套路 汪神,是浙江大学电气工程系的“风云人物”,曾因首度破解特斯拉汽车安全系统而名声大噪.本套题目是自 ...