作者:京东零售 贾玉西

一、前语

程序员A: MyBatis用过吧?

程序员B: 用过

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

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

程序员A: 哈哈,不至于。但运用进程中,因为业务数据校验不妥,确实可能会造满足表更新或许删去。

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

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

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

程序员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使用过程中带来的全表更新风险

第一步:读取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使用过程中带来的全表更新风险

四、MyBatis防止全表更新插件

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

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

第三章节中,说到MyBatis为运用者供给了四个扩展点,那么咱们就能够凭借扩展点来完结一个Mybatis防止全表更新的插件,详细怎样完结呢?这儿博主是运用StatementHandler阻拦器笼统出来一个SDK供需求方接入,阻拦器详细用法参阅度娘,这儿SDK完结流程为:获取预处理SQL及参数值 –> 替换占位符拼装完好SQL –> SQL句子规则解析 –> 校验是否为全表更新SQL。 当然还做了一些横向扩展,这儿放张图吧,更清晰些。

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

那么这个插件能阻拦哪些类型的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使用过程中带来的全表更新风险