如何规避mybatis使用过程中带来的全表更新风险

author author     2023-03-14     788

关键词:

作者:京东零售 贾玉西

一、前言

程序员A: MyBatis用过吧?

程序员B: 用过

程序员A: 好巧,我也用过,那你遇到过什么风险没?比如全表数据被更新或者删除了。

程序员B: 咔,还没遇到过,这种情况需要跑路吗?

程序员A: 哈哈,不至于。但使用过程中,由于业务数据校验不当,确实可能会造成全表更新或者删除。

程序员B: 喔,吓死我了,我们都是好人,不会做删库跑路类似蠢事,能展开讲讲这个风险怎样造成的吗?

程序员A: 好的,你能看出下面这段代码会有风险吗?

如何规避MyBatis使用过程中带来的全表更新风险_sql

程序员B: 平时大家都这样写的,也没看出啥风险呀!

程序员A: 假如DAO层没做非空校验,relationId字段传入为空,这段代码组装出来的是什么语句?

程序员B: update cms\\_relation\\_area_code set yn = 1 where yn = 0 我擦,全表被逻辑删除了!哥哥,我们的web应用数量多,代码行数几十万行,你怎么处理的呀,不会人力梳理代码吧?得累死......

程序员A: 昂,可以的,基于MyBatis的扩展点可以实现一款插件做到降低全表更新的风险,降低人工成本。

程序员B: 哥哥,要不讲讲MyBatis和实现的插件?

程序员A: 那必须嘞,技术是需要分享和互补的。

不知大家在使用MyBatis有没有过程序员A哥哥遇到的事件?好巧,本人也经历过跟程序员A小哥哥一样的境遇,初始思路也是人工梳理代码,后来经由架构师点拨能不能开发一款SDK统一处理,要不然就扛着身体去梳理这几十万行代码了。要不一起聊聊这块,共同成长~

一起先看下MyBatis原理吧?当然这部分比较枯燥,本篇文章也不会大废篇幅去介绍这块,简单给大家聊下基本流程,对MyBatis原理不感兴趣的同学可以直接跳到第三章往后看

那... 第二章我就简单开始淡笔介绍MyBatis了,在座各位好友没啥意见吧,想更深入了解学习,可以读下源码,或者阅读下京东架构-小傅哥手撸MyBatis专栏博客(地址:​​bugstack.cn​​)

二、MyBatis 原理

先来看下MyBatis执行的概括执行流程,就不逐步贴源码了,东西实在多...

//1.加载配置文件
InputStream inputStream =Resources.getResourceAsStream(“mybatis-config.xml”);
//2.创建 SqlSessionFactory 对象(实际创建的是 DefaultSqlSessionFactory 对象)
SqlSessionFactory builder =newSqlSessionFactoryBuilder().build(inputStream);
//3.创建 SqlSession 对象(实际创建的是 DefaultSqlSession 对象)
SqlSession sqlSession = builder.openSession();
//4.创建代理对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//5.执行查询语句
List<User> users = mapper.selectUserList();
//释放资源
sqlSession.close();
inputStream.close();

mybatis整个执行流程,可以抽象为上面5步核心流程,咱们这里只讲解XML开发的方式,注解的方式基本核心思想一致:

第一步:读取mybatis-config.xml配置文件。转化为流,这一步没有需要细说的。

第二步:创建SqlSessionFactory 对象。 实际创建的是DefaultSqlSessionFactory对象,这里SqlSessionFactory和DefaultSqlSessionFactory的关系为:SqlSessionFactory是一个接口,DefaultSqlSessionFactory是该接口的一个实现,也是利用了Java的多态特性。SqlSessionFactory是MyBatis中的一个重要的对象,汉译过来可以叫做:SQL会话工厂,见名知意,它是用来创建SQL会话的一个工厂类,它可以通过SqlSessionFactoryBuilder来获得,SqlSessionFactory是用来创建SqlSession对象的,SqlSession就是SQL会话工厂所创建的SQL会话。并且SqlSessionFactory是线程安全的,它一旦被创建,应该在应用执行期间都存在,在应用运行期间(也就是Application作用域)不要重复创建多次,建议使用单例模式。

第三步:创建 SqlSession 对象。 实际创建的是 DefaultSqlSession 对象,这里同上步,SqlSession为接口,DefaultSqlSession为SqlSession接口的一个实现类,SqlSession的主要作用是用来操作数据库的,它是MyBatis 核心 API,主要用来执行命令,获取映射,管理事务等。SqlSession虽然提供select/insert/update/delete方法,在旧版本中使用使用SqlSession接口的这些方法,但是新版的Mybatis中就会建议使用Mapper接口的方法,也就是下面要讲到的第四步操作。SqlSession对象,该对象中包含了执行SQL语句的所有方法,类似于JDBC里面的Connection。在JDBC中,Connection不直接执行SQL方法,而是生成Statement或者PrepareStatement对象,利用Statement或者PrepareStatement来执行增删改查方法;在MyBatis中,SqlSession可以直接执行增删改查方法,可以通过提供的 selectOne、 insert等方法,也可以获取映射器Mapper来执行增删改查操作,通过映射器Mapper来执行增删改查如第四步代码所示。这里需要注意的是SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域。

第四步:创建代理对象。 SqlSession一个重要的方法getMapper,顾名思义,这个方法是用来获取Mapper映射器的。什么是MyBatis映射器?MyBatis框架包括两种类型的XML文件,一类是配置文件,即mybatis-config.xml,另外一类是操作DAO层的映射文件,例如UserInfoMapper.xml等等。在MyBatis的配置文件mybatis-config.xml包含了标签节点,这里就是MyBatis映射器。也可以理解为标签下配置的各种DAO操作的mapper.xml的映射文件与DaoMapper接口的一种映射关系。映射器只是一个接口,而不是一个实现类。可能初学者可能会产生一个很大的疑问:接口不是不能运行吗?的确,接口不能直接运行,但是MyBatis内部运用了动态代理技术,生成接口的实现类,从而完成接口的相关功能。所以在第四步这里 MyBatis 会为这个接口生成一个代理对象。

第五步:执行SQL操作以及释放连接操作。

Emmm... 再补张图吧,刚刚的介绍感觉还没开始就结束了,通过下面这张图我们再深入了解下MyBatis整体设计(此图借鉴京东架构-小傅哥手撸MyBatis专栏)

如何规避MyBatis使用过程中带来的全表更新风险_sql_02

第一步:读取Mybatis配置文件。

第二步:创建SqlSessionFactory对象。 上面已经对SqlSessionFactory做了说明,但SqlSessionFactoryBuilder具体还没描述,SqlSessionFactoryBuilder是构造器,见名知意,它的主要作用便是构造SqlSessionFactory实例,基本流程为根据传入的数据流创建XMLConfigBuilder,生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。XMLConfigBuilder主要作用是解析mybatis-config.xml中的标签信息,如图中列举出的两个标签信息,解析环境信息及mapper.xml信息,解析mapper.xml时,Mybatis默认XML驱动类为XMLLanguageDriver,它的主要作用是解析select、update、insert、delete节点为完整的SQL语句,也是对应SQL的解析过程,XMLLanguageDriver在解析mapper.xml时,会将解析结果存储至SqlSource的实现类中,SqlSource是一个接口,只定义了一个 getBoundSql() 方法,它控制着动态 SQL 语句解析的整个流程,它会根据从 Mapper.xml 映射文件解析到的 SQL 语句以及执行 SQL 时传入的实参,返回一条可执行的 SQL。它有三个重要的实现类,对应图中写到的RawSqlSource、DynamicSqlSource及StaticSqlSource,其中RawSqlSource处理的是非动态 SQL 语句,DynamicSqlSource处理的是动态 SQL 语句,StaticSqlSource是BoundSql中要存储SQL语句的一个载体,上面RawSqlSource、DynamicSqlSource的SQL语句,最终都会存储到StaticSqlSource实现类中。StaticSqlSource的 getBoundSql() 方法是真正创建 BoundSql 对象的地方, BoundSql 包含了解析之后的 SQL 语句、字段、每个“#”占位符的属性信息、实参信息等。这里也重点介绍下Configuration对象,Configuration 的创建会装载一些基本属性,如事务,数据源,缓存,代理,类型处理器等,从这里可以看出 Configuration 也是一个大的容器,来为后面的SQL语句解析和初始化提供保障,也是Mybatis中贯穿全局的存在,后续我们要提到的Mybatis降低全表更新插件,也是基于这个对象来完成。其中解析mapper.xml这步最终作用便是将解析的每一条CRUD语句封装成对应的MappedStatement存放至Configuration中。

第三步:创建SqlSession对象。 创建过程中会创建另外两个东西,事务及执行器,SqlSession可以说只是一个前台客服,真正发挥作用的是Executor,它是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护,对SqlSession方法的访问最终都会落到Executor的相应方法上去。Executor分成两大类:一类是CachingExecutor,另一类是普通的Executor。CachingExecutor是在开启二级缓存中用到的,二级缓存是慎开启的,这里只介绍普通的Executor,普通的Executor分为三大类,SimpleExecutor、ReuseExecutor和BatchExecutor,他们是根据全局配置来创建的。SimpleExecutor是一种常规执行器,也是默认的执行器,每次执行都会创建一个Statement,用完后关闭;ReuseExecutor是可重用执行器,将Statement存入map中,操作map中的Statement而不会重复创建Statement;BatchExecutor是批处理型执行器,专门用于执行批量sql操作。总之,Executor最终是通过JDBC的java.sql.Statement来执行数据库操作。

第四步:获取Mapper代理对象。 上面也已经提到了这块用到的是jdk动态代理技术,这里MapperRegistry和MapperProxyFactory在解析mapper.xml已经被创建保存在了Configuration中,这步主要就是从MapperProxyFactory获取MapperProxy代理。其中MapperMethod主要的功能是执行SQL的相关操作,它根据提供的Mapper的接口路径,待执行的方法以及配置Configuration作为入参来执行对应的MappedStatement操作。

第五步:执行SQL操作。 这步就是执行执行对应的MappedStatement操作,Executor最终是通过JDBC的java.sql.Statement来执行数据库操作。但其实真正负责操作的是StatementHanlder对象,StatementHanlder封装了JDBC Statement 操作,负责对 JDBC Statement 的操作,它通过控制不同的子类,去执行完整的一条SQL执行与解析的流程。

三、MyBatis拦截器

Mybatis一共提供了四大扩展点,也称作四大拦截器插件,它是生成层层代理对象的一种责任链模式。这里代理的实现方式是将切入的目标处理器与拦截器进行包装,生成一个代理类,在执行invoke方法前先执行自定义拦截器插件的逻辑从而实现的一种拦截方式。每个处理器在Mybatis的整个执行链路中扮演的角色也不同,大家如果有想法可以基于这几个扩展点实现一款自己的拦截器插件。例如我们常用的一个分页插件pageHelper就是利用Executor拦截器实现的,有兴趣的可以自行阅读下pageHelper源码。MyBatis一共提供了四个扩展点:

Executor (update, query, ……)

Executor根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用。创建JDBC的Statement连接对象,传递给StatementHandler对象。这里Executor又称作 SQL执行器

· StatementHandler (prepare, parameterize, ……)

StatementHandler对于JDBC的PreparedStatement类型的对象,创建的过程中,这时的SQL语句字符串是包含若干个 “?” 占位符。这里StatementHandler又称作SQL 语法构建器

· ParameterHandler (getParameterObject, ……)

ParameterHandler用于SQL对参数的处理,这步会通过TypeHandler将占位符替换为参数值,接着继续进入PreparedStatementHandler对象的query方法进行查询。这里ParameterHandler又称作参数处理器

· ResultSetHandler (handleResultSets, ……)

ResultSetHandler进行最后数据集(ResultSet)的封装返回处理。这里ResultSetHandler又称作结果集处理器

如何规避MyBatis使用过程中带来的全表更新风险_SQL_03

四、MyBatis防止全表更新插件

上面说到程序员A小哥哥遇到过历史业务参数因校验问题造成了全表更新的风险,梳理代码成本又过高,不符合当下互联网将本增效的理念。那么有没有一种成本又低,效率又高,又能通用的产品来解决此类问题呢?

当然有了!!! 不然这篇帖子搁这凑绩效呢? 哈哈... 不好笑不好笑,见谅。

第三章节中,提到MyBatis为使用者提供了四个扩展点,那么我们就可以借助扩展点来实现一个Mybatis防止全表更新的插件,具体怎么实现呢?这里博主是使用StatementHandler拦截器抽象出来一个SDK供需求方接入,拦截器具体用法参考度娘,这里SDK实现流程为:获取预处理SQL及参数值 --> 替换占位符组装完整SQL --> SQL语句规则解析 --> 校验是否为全表更新SQL。 当然还做了一些横向扩展,这里放张图吧,更清晰些。

如何规避MyBatis使用过程中带来的全表更新风险_xml_04

那么这个插件能拦截哪些类型的SQL语句呢?

·无where条件:update/delete table 

·逻辑删除字段:update/delete table where yn = 0 //yn为逻辑删除字段

·拼接条件语句:update/delete table where 1 = 1

·AND条件语句:update/delete table where 1 = 1 and 1 <> 2

·OR 条件语句:update/delete table where 1 = 1 or 1 <> 2

然后聊下怎么接入吧:

4.1 检查项目依赖

scope为provided的请在项目中加入该jar包依赖,此插件默认引入p6spy、jsqlparser依赖,如遇版本冲突请排包

<dependency>    
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>$slf4j.version</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>$p6spy.version</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>$mybatis.version</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>$mybatis-spring.version</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>$jsqlparser.version</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>$spring.core.version</version>
<scope>provided</scope>
</dependency>

4.2 项目中引入防止全表更新依赖SDK

<dependency>    
<groupId>com.jd.o2o</groupId>
<artifactId>o2o-mybatis-interceptor</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

4.3 项目中添加配置

springboot项目使用方式: 配置类中加入拦截器配置

@Configuration
public class MybatisConfig
@Bean
ConfigurationCustomizer configurationCustomizer()
return new ConfigurationCustomizer()
@Override
public void customize(org.apache.ibatis.session.Configuration configuration)
FullTableDataOperateInterceptor fullTableDataOperateInterceptor = new FullTableDataOperateInterceptor();
//表默认逻辑删除字段,按需配置,update cms set name = "zhangsan" where yn = 0,yn为逻辑删除资源,此语句被认为是全表更新语句
fullTableDataOperateInterceptor.setLogicField("yn");
//白名单表,按需配置,配置的白名单表不拦截该表全表更新操作
fullTableDataOperateInterceptor.setWhiteTables(Arrays.asList("tableName1","tableName2"));
//个别表的逻辑删除字段映射,如果配置此项,此表逻辑删除字段优先走该表配置,key为表名,value为该表的逻辑删除字段名,每对key-value以英文逗号分隔配置
Map<String,String> tableToLogicFieldMap = new HashMap<>();
tableToLogicFieldMap.put("tableName3","ynn");
tableToLogicFieldMap.put("tableName4","ynn");
fullTableDataOperateInterceptor.setTableToLogicFieldMap(tableToLogicFieldMap);
//配置拦截器
configuration.addInterceptor(fullTableDataOperateInterceptor);

;

传统SSM项目使用方式: 在mybatis.xml中追加plugin配置

<configuration>      
<plugins>
<plugin interceptor="com.jd.o2o.cms.mybatis.interceptor.FullTableDataOperateInterceptor">
//表默认逻辑删除字段,按需配置,update cms set name = "zhangsan" where yn = 0,yn为逻辑删除字段,此语句被认为是全表更新语句
<property name="logicField" value="yn"/>
//白名单表,按需配置,配置的白名单表不拦截该表全表更新操作
<property name="whiteTables" value="tableName1,tableName2"/>
//个别表的逻辑删除字段映射,如果配置此项,此表逻辑删除字段优先走该表配置,key为表名,value为该表的逻辑删除字段名,每对key-value以英文逗号分隔配置
<property name="tableToLogicFieldMap" value="key1:value1,key2:value2"/>
</plugin>
</plugins>
</configuration>

4.4 添加日志输出

该插件有四处输出error日志,具体可看源码

<Logger name="com.jd.o2o.cms.mybatis.interceptor" level="error" additivity="false">    
<AppenderRef ref="RollingFileError"/>
</Logger>

4.5 性能及接入说明

大家最关心的可能是,接入这个SDK后,对我们数据库操作的性能有多大影响,这里针对性能做下说明:

•select:无性能影响

•insert:不足千分之一毫秒

•update:约为0.02毫秒

•delete:约为0.02毫秒

然后就是对接入的风险的考虑,如果为该插件解析过程中的异常,该插件直接catch交由MyBatis进行下个执行链的处理,对业务流程无影响,代码为证:

如何规避MyBatis使用过程中带来的全表更新风险_sql_05

技术风险规避方法

本博客会同步迁移到微信公众号:程序猿小哥基础编码1.并发控制,默认使用悲观锁,一锁二判三更新,乐观引入须谨慎。2.幂等拦截,幂等新老要兼容,字段约束需一致,异常场景防击穿。3.状态推进,流转设计要完整,状态推进凭指... 查看详情

了解爬虫的风险与以及如何规避风险-java网络爬虫系统性学习与实战系列

了解爬虫的风险与以及如何规避风险-Java网络爬虫系统性学习与实战系列(3)文章目录概述法律风险民事风险刑事风险个人信息的法律风险著作权的风险(文章、图片、影视等数据)5不要3准守什么情况下,爬虫业务是完全合法... 查看详情

静默活体检测能力,有效规避用户实名认证环节风险(代码片段)

静默式活体检测,是华为HMSCore机器学习服务所属的人脸活体检测能力,即无需用户配合做出张嘴、扭头、眨眼等动作,便可实时捕捉人脸,快速判断是否为活体,用户使用过程便捷,综合体验感较佳。技... 查看详情

攻防演习紫队第三篇之风险规避措施(代码片段)

文章目录风险规避措施0x01演习限定攻击目标,不限定攻击路径0x02除授权外,演习不允许使用拒绝服务攻击0x03网页篡改攻击方式说明0x04演习禁止采用的攻击方式0x05攻击木马使用要求0x06非法攻击阻断及通报摘抄风险规避... 查看详情

灵魂拷问:如何规避生产环境的性能测试风险?

Hi,大家好,常言道,上线一时爽,事后火葬场。隐秘Bug的哲学之道:不知道藏在哪里,不知道有多少,总是在你准备休息的时候出现。生产环境一旦出问题,内心一阵发凉,当天必须解决,... 查看详情

对bigtable进行全表更新,导致replication同步数据的过程十分缓慢

...的。在Logreader将Commands插入到distribution.dbo.MSrepl_commands的过程中 查看详情

golang规避sql注入风险

sql您可以通过提供SQL参数值作为包函数参数来避免SQL注入风险。包中的许多函数sql为SQL语句和要在该语句的参数中使用的值提供参数(其他函数为准备好的语句和参数提供参数)。以下示例中的代码使用?符号作为id参数的占位符... 查看详情

如何规避适配风险?以《乱世王者》为例,探秘手游兼容性测试之路

欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~作者:LaneLi,腾讯适配测试负责人、WeTest专家兼容测试负责人由腾讯游戏云发布在云+社区项目背景《乱世王者》是一款历史架空背景的战争策略手游,最大程度的还原策... 查看详情

nft是泡沫吗-市场趋势-如何筛选nft产品及规避风险

为什么一张图片可以价值连城?越来越多的年轻人和大机构开始涌入这个市场,NFT俨然成为了当下最火的潮流,这让圈外人不得不有此一问,为什么这些基于JPG图片或是基于像素生成的NFT作品会价值连城?【.... 查看详情

由于一个简单的 IN 语句,大量的全表扫描(大约 600 次)

】由于一个简单的IN语句,大量的全表扫描(大约600次)【英文标题】:Lotoffulltablescans(around600)becauseofasimpleINstatement【发布时间】:2015-06-2311:22:26【问题描述】:我的查询中有2个CTE。在查询结束时,我只需加入它们并将结果写入... 查看详情

Bootstrap:容器中带有列的全宽网格

】Bootstrap:容器中带有列的全宽网格【英文标题】:Bootstrap:Fullwidthgridwithcolumnsincontainer【发布时间】:2017-01-2423:54:17【问题描述】:我想创建一个全宽布局,左侧为蓝色半边,右侧为红色半边。之后我想在布局内但在容器内添加... 查看详情

渗透测试介绍和风险规避点(代码片段)

...测试:灰盒测试:0x02可能存在的风险描述0x03风险规避方法:摘抄0x01渗透测试介绍渗透性测试根据测试者在测试前掌握被测信息多少的不同可分为:黑盒测试:黑盒测试也称为外部测试。在进行黑盒测试时候&#... 查看详情

react-native中带有/ flexbox的全宽按钮

】react-native中带有/flexbox的全宽按钮【英文标题】:Fullwidthbuttonw/flex-boxinreact-native【发布时间】:2016-03-0420:24:59【问题描述】:我是flexbox的新手,无法在react-native中生成全宽按钮。一天多来,我一直试图自己弄清楚这一点,并且... 查看详情

警惕oracle索引优化时陷阱之无效的索引范围扫描(indexrangescan)导致的全表扫描(代码片段)

生产环境慢查询统计中,发现表STATUS的MILESTONE字段条件查询时进行了全表扫描。表STATUS的MILESTONE字段定义如下:针对上述问题创建索引:createindexSTATUS_MILESTONEonSTATUS("MILESTONE")tablespaceDFS_INDEX2;分析执行计划发现问... 查看详情

mysql优化-数据类型不匹配导致的全表扫描(代码片段)

一条sql执行时间特别长,很多时候页面都直接504timeout,sql内容大致如下,其中a表的数据有2000w+数据,b表有600w+数据SELECT a.*FROM aWHERE a.user_id=123ANDa.idNOTIN( SELECT b.pay_record_idASpay_record_id FR 查看详情

如何利用开源风控系统(星云)防止撞库?(代码片段)

  在企业发展过程中,日益增多的业务形态往往会招致新的业务风险。简单的业务防护已经不足以解决问题。一套完整的业务风控系统可以帮助企业有效的规避风险,降低损失。TH-Nebula(星云)是威胁猎人开源的业务风控... 查看详情

android插件化使用pluginkiller帮助应用开发者规避发布的apk安装包被作为插件的风险(验证应用是否运行在插件化引擎中)(代码片段)

文章目录前言一、应用开发者规避APK安装包被作为插件二、检测插件化环境1、检查AndroidManifest.xml清单文件2、检查运行时信息3、检查生成的目录4、检查组件前言在上一篇博客【Android插件化】插件化技术弊端(恶意插件化程序的... 查看详情

oracle怎么优化

参考技术Aoracle优化方法如下:去掉不必要的大型表的全表扫描。缓存小型表的全表扫描。检验优化索引的使用。检验优化的连接技术。尽可能减少执行计划的Cost,在含有子查询的SQL语句中,要特别注意减少对表的查询。 查看详情