Mybatis 逆向工程使用姿势不对文档全被清空,一怒之下写了个插件……

2020 年 6 月 12 日 CSDN
作者 | why技术 
责编 | 王晓曼
来源 | why技术
 
荒腔走板

时间过的真是快,一周又要结束了。那么,你比上周更博学了吗?先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩。
上面这张照片是我上周六的傍晚写完文章后出门跑步的时候拍的。
当时我路过这个地方看到这个画面的时候一眼就被吸引住了,直接停下了奔跑的脚步。
这是一个还没有开发完成的草坪。远处的高楼,就是成都的软件园区。软件园和草坪之间只有一条河间隔。河的对岸是工作,河的这边是生活。
我拍这个照片的时候只是觉得和谐,随手一拍。但是现在再看,不知道为什么感觉到的却是深邃的孤独。
一个玩手机的阿姨,一只孤独的狗。热闹是他们的,我什么都没有。
我还挺喜欢这张照片的,好了,说文章吧。

这锅只能自己背了

你用过 Mybatis 逆向工程(mybatis-generator-maven-plugin)生成相关文件吗?
就像这样的:
可以看到逆向工程帮我们生成了实体类、Mapper 接口和 Mapper.xml。
用起来真的很方便,我用了好几年了,但是前段时间翻车了。
具体是怎么回事呢,我给大家摆一下。
先说一下需求吧。就是在做一次借据数据迁移的过程中,要先通过 A 服务的接口拿到所有的借据和对应的还款计划数据,然后再对这些借据进行核查,如果不满足某些添加,就需要从表中删除借据和对应的还款计划
借据和对应的还款计划存放在两张表中,用借据号来关联。
而上线之后,我在一片欢声笑语中把还款计划表清空了,而这个必定出现的问题,在测试阶段同学还没有测试出来。
事情发生后我赶紧找到了 DBA 协助修复数据:
是怎么回事呢,为了模拟这个场景,我在本地创建了两张表,订单表(orderInfo)和订单扩展表(orderInfoExt),他们之间用订单号进行关联:
仅仅是做演示,所以两张表是非常简单的.
我们假设现在表里面的这条订单号为 2020060666666 的数据经过判断是错误数据,我当时写的代码体现在单元测试里面是这样的:
看出问题了吗?
第 42 行用的 example 对象还是 OrderInfo 的 example。而真正的 OrderInfoExt 对象的 exampleExt  对象没有进行任何赋值的操作。
为什么会出现这样的乌龙呢?
都怪 idea 太智能了!(强行找个借口)
我只需要打一个 ex 然后回个车.... example 就出现在代码里面了。
而这种没有参数的 example 传进去,在 mapper.xml 里面是这样处理的:
执行一下,看看效果:
看到 delete from order_info_ext 语句。你说你慌不慌?
当然在线上的服务器肯定是看不到执行的 SQL 的,但是当报警短信一条一条接着来的时候,当连上数据库一看表,发现数据没了的时候。
你说你慌不慌?
反正我一刷新后发现表里没数据了,一股凉意从脚板心直冲天灵盖。这种时候都还是要小小的心慌一下,先大喊一声“卧槽!数据怎么没了?”
然后赶紧报备,准备找 DBA 捞数据吧。
还好,本次误删不影响正常业务。
数据恢复过程就不说了,聊一下这事发生后我的一点思考吧。
哦,对了,还得说一下测试同学为什么没有发现这个问题。这个问题确实是一个必现的问题,测试案例上也写了这个测试点。
但是测试同学查看数据的时候用的是 select 语句,查询条件给的是确实需要被删除的数据 。
然后分别在两个表里面执行后发现:数据确实是没了。
是的,是数据确实是没了。整个表都干净了。
看着测试妹子惊慌失措的样子,我还能怎么说呢?
这锅,不甩了,我自己背下来吧。
 
重新审视逆向工程

我们先看看逆向工程帮我们生成的接口:
我相信用过 mybatis 逆向工程的朋友们,一看到这几个接口就知道了:哟,这都是老朋友了。
当我再去重新审视这些接口的时候我会发现其实还有会有一些问题的。
比如 delete 这样的高危语句我们还是需要尽量的手写 xml。
比如 updateByExample 同样存在由于误操作没有 where 条件,导致全表更新的情况。
比如 select 语句是查出了整个对象,但是有时间我们可能只需要对象里面的某个值而已。
比如 select 语句针对大表、关键表操作的时候,不能从代码的角度限定 SQL 必须带上索引字段查询。
上面的这些问题我们怎么处理呢?
我的建议是不要使用 mybatis 的逆向工程,全都手写。
开个玩笑。我们肯定不能因噎废食,何况逆向工程确实是帮我们做了很多工作,极大的方便我们这样的 CRUD Boy 进行 CRUD。
所以,我想 mybatis 的逆向工程肯定是有什么配置来控制生成哪些接口的,别问为什么,问就是直觉。
因为要是让我去开发这样的一个插件,我肯定也会提供对应的开关配置。
我现在的想法是不让它给我生成 delete 相关的接口,这个接口用起来我心里害怕。
所以怎么配置呢?
我们去它的 DTD 文件里面找一下嘛:
这个文件不长,一共也才 213 行,你能发现这一块东西:
你用脚指头想也能知道,这就是我们要找的开关配置。从 DTD 文件的描述中来看,这个几个参数是配置在 table 标签里面的
我们去试一下:
果然是这样的。然后我们进行相关配置如下:
再生成一下:
果然,delete 相关的接口没了。
然后我们程序中真的需要 delete 操作的时候,再自己去手写 xml 文件。
那你自己写的 xml 文件也忘记写 where 条件了这么办?
这个月工资别领了。自己好好反思反思。
当然,就算你真的忘记写了,下面这个拦截器还能给你兜个底,帮你一把。

Mybatis 拦截器使用

其实这个方案是我想到的第一个方案。导致上面问题的原因很简单嘛,就是执行了delete 语句却没有 where 条件。
那么我们可以拦截到这个 SQL 语句,然后对其进行两个判断:
1. 是否是 delete 语句。
2. 如果是,是否包含 where 条件。
那么问题来了,我们怎么去拦截到这个 SQL 呢?
答案就是我们可以开发一个 mybatis 插件呀,就像分页插件那样。
插件,听起来很高端的样子,其实他就是个拦截器。实现起来非常简单。
先去官网上看一下:
中文:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
英文:https://mybatis.org/mybatis-3/configuration.html
在官网上,对于插件这一模块的描述是这样的:
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
正如官网说的这样,插件开发、使用起来是非常简单的。只需要三步:
1. 实现 Interceptor 接口。
2. 指定想要拦截的方法签名。
3. 配置这个插件。

Mybatis 插件开发

基于上面这三步,大家先看一下我们这插件怎么写,以及这个插件的效果。
先说明一下本文涉及到的源码 mybatis 版本是 3.4.0。
本文用拦截器的目的是判断 delete 语句中是否有 where 条件。所以,开发出来的插件长这样:
再来一个复制粘贴直接运行版本:
   
   
     
@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.classObject.class}),
})
public class CheckSQLInterceptor implements Interceptor {

    private static String SQL_WHERE = "where";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取方法的第0个参数,也就是MappedStatement。@Signature注解中的args中的顺序
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        //获取sql命令操作类型
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        final Object[] queryArgs = invocation.getArgs();
        final Object parameter = queryArgs[1];
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        String sql = boundSql.getSql();
        if (SqlCommandType.DELETE.equals(sqlCommandType)) {
            //格式化sql
            sql = sql.replace("\n""");
            if (!sql.toLowerCase().contains(SQL_WHERE)) {
                sql = sql.replace(" """);
                log.info("删除语句中没有where条件,sql为:{}", sql);
                throw new Exception("删除语句中没有where条件");
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
再把插件注册上(注册插件还有其他的方法,后面会讲到,这里只是展示Bean注入的方式):
我们先看看配上插件后的执行效果:
可以看到日志中输出了:
删除语句中没有where条件,sql为:delete from order_info_ext
并抛出了异常。
这样,我们的扩展表的数据就保住了。在测试阶段,测试同学就一定能扯出来问题,瞟一眼日志就明白了。
就算测试同学忘记测试了,在生产上也不会执行成功,抛出异常后还会有报警短信通知到相应的开发负责人,及时登上服务器去处理。
功能实现了,确实是非常的简单。
我们再说回代码,你说说看:当你拿到上面这段代码后,最迷惑的地方是哪里?
其中的逻辑是很简单的了。没有什么特别的地方,我想大多数人拿到这段代码迷惑的地方在于这个地方吧:
这个 @Intercepts 里面的 @Signature 里面为什么要这样配置?
我们先看看 @Intercepts 注解:
里面是个数组,可以配置多个 Signature。所以,其实这样配置也是可以的:
关键的地方在于 @Signature 怎么配置:
这个问题,我们放到下一节去讨论。

Mybatis插件的原理

上面一小节我们知道了对于开发插件而言,难点在于 @Signature 怎么配置。
其实这也不能叫难点,只能说你不知道能配置什么,比较茫然而已。这一小节就来回答这个问题。
要知道怎么配置就必须要了解mybatis 这四大对象:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。
官网上说:
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
2. ParameterHandler (getParameterObject, setParameters)
3. ResultSetHandler (handleResultSets, handleOutputParameters)
4. StatementHandler (prepare, parameterize, batch, update, query)
那官网上说的这四大对象分别是拿来干啥用的呢?
1. Executor:Mybatis 的执行器,用于进行增删改查的操作。
2. ParameterHandler :参数处理器,用于处理 SQL 语句中的参数对象。
3. ResultSetHandler:结果处理器,用于处理 SQL 语句的返回结果。
4. StatementHandler :数据库的处理对象,用于执行SQL语句
知道拦截的四大对象了,我们就可以先揭秘一下上面的这个注解配置的是啥了:
1. type 字段存放的是 class 对象,其取值范围就是上面说的四大对象。
2. method 字段存放的是 class 对象的具体方法。
3. args 存放的是具体方法的参数。
看到这几个参数你想到了什么?有没有条件反射式的想到反射?如果没有的话你再咂摸咂摸,看看能不能品出一点反射的味道。
本文用拦截器的目的是判断 delete 语句中是否有 where 条件,因此经过上面的分析,Executor 对象就能满足我们的需求。
所以在本文示例中 @Signature 的 type 字段就是 Executor.class。
那 method 字段我们放哪个方法呢?放 delete 吗?
这就得看看 Executor 对象的方法有哪些:
可以看到其中并没有 delete 方法,和 SQL 执行相关的,看起来只有 query和 update。
但是,我们可以大胆猜测一下呀:delete 也是一种 update。
接着去求证一下就行:
可以看到 delete 方法确实是调用了 update 方法。
所以在本文案例中 @Signature 的 method 字段放的是 update 方法。
已经知道具体的方法了,那 args 放的就是方法的入参,所以这段配置就是这样来的:
真的,我觉得这属于手摸手教学系列了。经过这个简单的案例,我希望大家能做到一通百通。
接下来带大家看看我们常用的分页插件 pageHelper 是怎么做的吧。
其实你用脚指头也能想到,分页插件肯定是拦截的查询方法,我们只是需要去验证一下就可以。
引入 pageHelper 后可以看到 Interceptor 的多了两个实现:
我们看一下 PageInterceptor 方法吧:
对吧,拦截了两个 query 方法,一个参数是 4 个,一个参数是 6 个: 
同时,在 intercept 的实现里面有一部分是这样写的:
4 个参数和 6 个参数是做了单独处理的,至于为什么要这样处理,至于为什么要拦截两个 query 方法,说起来又是一个很长的故事了。
详细的可以看看这个链接:
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md
好了,还是那句话:如果要写出好的 mybatis 插件,必须知道 @Signature 怎么去配置。配置后能拦截哪些东西,你心里应该是有点数的。

Mybatis插件的原理

前面我们知道拦截器怎么写了,接下来简单的分析一波原理。
前几天我看到一个观点是说看开源框架的源码建议从 mybatis 看起。我是很赞成这个观点的,确实是优雅,而容易看懂。能品出很多设计模式的使用。
一句话总结 mybatis插件的原理就是:动态代理加上责任链。
先看一下 Plugin 类的动态代理:
标号为 ① 的地方一看就知道,InvocationHandler,JDK 动态代理,没啥说的。
标号为 ② 的地方是 wrap 方法,生成 Plugin 代理对象。
标号为 ③ 的地方是 invoker 方法,圈起来的目的是想说是在这里判断当前方法是否是需要被拦截的方法。如果是则用代理对象走拦截器逻辑,如果不是则用目标对象,走正常逻辑。
给大家看一下这个地方的 debug 效果:
一个平平无奇的 if 判断,是拦截器的关键。为什么这个地方多说了几句呢?
因为其实这就是细节的地方。当面试的时候面试官问你:mybatis 是怎么判断是否需要拦截这个方法的时候你能答上来。说明你是真的看过源码。
责任链是怎么体现的呢?
就是这个地方:
org.apache.ibatis.plugin.InterceptorChain
你看又学到一招,mybatis 里面的设计模式还有责任链。
我们看一下 pluginAll 方法的调用方:
这个地方就体现出之前官网说的了:
插件是作用于这四大对象的:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。
上面框起来的这四个框,就是插件调用的地方。
那么插件在什么时候被加载,或者说什么是被注册上的呢?
还是回到拦截链这个类上去:
pluginAll 方法我们已经知道有哪些地方调用了。这个方法里面其实还有两个考点。
1. 第一就是 interceptor 这个 List 集合的定义,用了 final 修饰。所以要注意 final 修饰基本类型和引用类型的区别,被 final 修饰的引用类型变量内部的内容是可以发生变化的。
2. 第二就是 getInterceptors 返回的是一个不可修改的 List 。所以,要对集合 interceptors 进行修改,只能通过 addInterceptor 方法进行元素添加,保证了这个集合是可控的。
所以,我们只需要知道哪里调用了 addInterceptor 方法,哪里就是插件被注册的地方。
一个是 SqlSessionFactoryBean ,一个是 XMLConfigBuilder。
使用 XML 配置是这样的:
熟悉 Mybatis 的朋友们肯定知道,无非就是对于标签的解析而已。
解析到 plugins 标签,则进入 pluginElement 方法中,在这个方法里面调用 addInterceptor:
本文没有使用 XML 的形式配置,所以我们主要看一下 SqlSessionFactoryBean。
怎么看呢?
不要盲目的走入源码,加个断点看调用链,跟着调用链去走就很清晰了。
在这个地方加一个断点:
然后 debug 起来,你就可以看到整个调用链了:
然后我们根据上面的调用链,我们就可以找到源头了:
在 MybatisAutoConfiguration 的构造方法里面初始化了 interceptors。
而 interceptorsProvider.getIfAvailable() 方法也解释了为什么我们只需要在程序里面这样注入我们的拦截器就可以被找到了:
对 getIfAvailable 方法不熟悉的朋友可以去补一下这块的知识,我这里只是给大家看一下这个方法上的注释:
当然,你这样去注入的话有可能会不生效,你就会大骂一声:写的什么垃圾玩意,配置上了也不对呀。
别着急呀,我还没说完呢。你看看是不是有自定义的 SqlSessionFactory 在项目里。
看一下注入 SqlSessionFactory 的源码上面的那个注解了吗?
@ConditionalOnMissingBean ,看名字也知道了,当你的项目里面没有自定义的 SqlSessionFactory 的时候,才会由源码给你注入,这个时候才会正在的注册上插件:
如果你有自定义的 SqlSessionFactory,那么请手动调用 factory.setPlugins 方法。
所以,总结一下插件的三种配置方法:
1. xml方式配置。
2. 如果没有自定义 SqlSessionFactory 直接 @Bean 注入拦截器即可。
3. 如果有自定义 SqlSessionFactory  需要在自定义的地方手动调用 factory.setPlugins 方法。
其实我尝试过第四种方法,在application.properties 里面配置:
这种配置方式才是符合 SpringBoot 思想的配置。才是真正的丝滑,润物无声的丝滑。
可惜,我配置上后,点击到对应的源码地方一看:
它调用的是 getInterceptors 方法,我就知道肯定是有问题了:
果然,运行起来会报这样的错误:
Failed to bind properties under 'mybatis.configuration.interceptors' to java.util.List<org.apache.ibatis.plugin.Interceptor>
找了一圈原因,最后发现了这个 issue:
github.com/mybatis/spring-boot-starter/issues/180
这个“奇异博士”头像的用户提出了和我一样的问题:
然后下面的回答是这样的:
别问,问就是不支持。请使用 @Bean 的方式。
【END】

更多精彩推荐

苹果或在 WWDC 宣布放弃英特尔转向自研 5nm ARM 芯片,这次时机成熟了?

滴滴辟谣被美团收购;苹果提交认证 9 款新手机;VS Code 1.46 发布 | 极客头条

Linux 之父怒删工程师提交的补丁,称“太蠢了”网友:怼得好!
干货!3 个重要因素,带你看透 AI 技术架构方案的可行性!
干货 | 大白话彻底搞懂 HBase RowKey 详细设计

热评 | 警惕新基建热潮中的区块链项目烂尾

你点的每个“在看”,我都认真当成了喜欢
登录查看更多
0

相关内容

SQL 全名是结构化查询语言,是用于数据库中的标准数据查询语言,IBM 公司最早使用在其开发的数据库系统中。
【论文推荐】文本摘要简述
专知会员服务
68+阅读 · 2020年7月20日
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
Python地理数据处理,362页pdf,Geoprocessing with Python
专知会员服务
113+阅读 · 2020年5月24日
【干货书】数值计算C编程,319页pdf,Numerical C
专知会员服务
67+阅读 · 2020年4月7日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
知识神经元网络 KNN(简介),12页pdf
专知会员服务
14+阅读 · 2019年12月25日
【GitHub实战】Pytorch实现的小样本逼真的视频到视频转换
专知会员服务
35+阅读 · 2019年12月15日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
开源书:PyTorch深度学习起步
专知会员服务
50+阅读 · 2019年10月11日
PC微信逆向:两种姿势教你解密数据库文件
黑客技术与网络安全
16+阅读 · 2019年8月30日
Mask-RCNN模型的实现自定义对象(无人机)检测
计算机视觉life
17+阅读 · 2019年8月12日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
请快点粘贴复制,这是一份好用的TensorFlow代码集
当你的头出现在A片演员的身体上
PingWest品玩
6+阅读 · 2019年1月6日
已删除
AI科技评论
4+阅读 · 2018年8月12日
干货 | Python 爬虫的工具列表大全
机器学习算法与Python学习
10+阅读 · 2018年4月13日
宅男福音deepfakes开源了
AI前线
9+阅读 · 2018年1月31日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Arxiv
8+阅读 · 2018年4月8日
Arxiv
7+阅读 · 2018年3月22日
VIP会员
相关VIP内容
【论文推荐】文本摘要简述
专知会员服务
68+阅读 · 2020年7月20日
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
Python地理数据处理,362页pdf,Geoprocessing with Python
专知会员服务
113+阅读 · 2020年5月24日
【干货书】数值计算C编程,319页pdf,Numerical C
专知会员服务
67+阅读 · 2020年4月7日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
知识神经元网络 KNN(简介),12页pdf
专知会员服务
14+阅读 · 2019年12月25日
【GitHub实战】Pytorch实现的小样本逼真的视频到视频转换
专知会员服务
35+阅读 · 2019年12月15日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
开源书:PyTorch深度学习起步
专知会员服务
50+阅读 · 2019年10月11日
相关资讯
PC微信逆向:两种姿势教你解密数据库文件
黑客技术与网络安全
16+阅读 · 2019年8月30日
Mask-RCNN模型的实现自定义对象(无人机)检测
计算机视觉life
17+阅读 · 2019年8月12日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
请快点粘贴复制,这是一份好用的TensorFlow代码集
当你的头出现在A片演员的身体上
PingWest品玩
6+阅读 · 2019年1月6日
已删除
AI科技评论
4+阅读 · 2018年8月12日
干货 | Python 爬虫的工具列表大全
机器学习算法与Python学习
10+阅读 · 2018年4月13日
宅男福音deepfakes开源了
AI前线
9+阅读 · 2018年1月31日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Top
微信扫码咨询专知VIP会员