作者:京东批发 贾玉西

一、前言

程序员A: MyBatis用过吧?

程序员B: 用过

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

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

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

程序员B: 喔,吓死我了,咱们都是坏蛋,不会做删库跑路相似蠢事,能开展讲讲这个危险怎么造成的吗?

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

程序员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蕴含了<mappers></mappers>标签节点,这里就是MyBatis映射器。也能够了解为<mappers></mappers>标签下配置的各种DAO操作的mapper.xml的映射文件与DaoMapper接口的一种映射关系。映射器只是一个接口,而不是一个实现类。可能初学者可能会产生一个很大的疑难:接口不是不能运行吗?确实,接口不能间接运行,然而MyBatis外部使用了动静代理技术,生成接口的实现类,从而实现接口的相干性能。所以在第四步这里 MyBatis 会为这个接口生成一个代理对象。

第五步:执行SQL操作以及开释连贯操作。

Emmm... 再补张图吧,刚刚的介绍感觉还没开始就完结了,通过上面这张图咱们再深刻理解下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避免全表更新插件

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

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

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

那么这个插件能拦挡哪些类型的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我的项目应用形式: 配置类中退出拦截器配置

@Configurationpublic 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进行下个执行链的解决,对业务流程无影响,代码为证: