查看原文
其他

mybatis开发,你用 xml 还是注解?我 pick ...

why技术 why技术 2022-08-30

持续输出原创文章,点击蓝字关注我吧




这是why的第 48 篇原创文章


荒腔走板

大家好,我是 why 。老规矩,在技术分享开始之前,先荒腔走板,聊点别的。


上周我写的这篇文章《我告诉你这书的第 3 版到底值不值得买?》居然被《深入理解Java虚拟机》的作者周志明先生看到了,还给我赞赏并留言给我说:作者表示感谢,真心的。


说实话,我看到这个赞赏的时候我都震惊了。有一种和大神产生了交集的感觉。


其实上周这篇文章是出版社找到我说送我一本第三版,让我看看,然后写个观后感就行。


恰好,在他们没有找到我之前我也是有这样的打算的。


我不是为了白嫖出版社几本书,而是我早在去年年底打算买第三版后,这篇文章就一直在着手准备了。


在机缘巧合之下,即完成了自己的计划,又获得了出版社的几本书,通过出版社,又勾搭上了本书作者,不仅获得了作者的赞赏还得到一本签名版。


哎,这疯狂而又操蛋的人生呀。


所以你问我做公众号收获了什么?


说实在的,我没有通过公众号挣到几个钱。但是我收获的是与一群志同道合的原创作者同行的机会、是读者读完文章后对我的文章的指点与赞扬、是偶尔与几位业界大佬之间互动的惊喜。


仅此而已。


好了,说回文章。


两种形式,三种写法

最近在看公司的一些项目的时候发现有的项目里面的 mybatis 是基于注解开发的。而我个人的习惯是基于 xml 文件开发。


所以对于基于注解开发的原理不太了解,于是去翻看了一下相关源码,形成此文。


本文主要介绍基于 mybatis 开发的两种形式,三种写法。


其中两种形式是指:


  1. 基于 xml 文件。

  2. 基于注解开发。


三种写法是指除了 xml 的形式外,注解又有两种不同的写法,它们的实现原理也略有不同,拿 Select 语句举例,就有两种注解 @Select、@SelectProvider 。


演示示例

先上一个演示示例给大家直观的感受一下:


首先,我们有个用户表,包含这些字段和这样一条数据:


然后我们搞个接口类,用三种方式去查询用户的年龄,具体如下:


xmlQueryAgeByName 方法是使用 xml 的方法去查询用户年龄,对应的 xml 如下:


annotationQueryAgeByName 方法是使用 @Select 注解去查询用户的年龄,SQL 就写在注解里面:


classQueryAgeByName 方法是使用 @SelectProvider 注解去查询用户的年龄,可以看到注解里面有个 type 字段,对应一个 class 类。一个 method 字段,对应 class 类中的一个方法:


其中 UserInfoSql 类如下:


然后,再来一个测试用例,把三个方法都测试一下:


最后的输出结果如下:

xmlQueryAgeByName whyAge = 18
annotationQueryAgeByName whyAge = 18
classQueryAgeByName whyAge = 18


测试用例就演示完成了,是一个极简的用例。


我就是基于这个案例去分析源码的,在分析之前,其实有点经验的老哥也能看出来了,我们先撇开常规的 xml 文件的形式不谈。


基于 @Select 注解的接口, SQL 就在注解里面,所以我们只需要通过反射取出注解里面的 SQL 进行分析就行了。


基于 @SelectProvider 注解的接口,SQL 虽然在一个类的方法中,但是注解上都告诉你是哪个类的哪个方法了,所以,一定是基于反射去取出方法里面的 SQL 的。


接下来,我们就是去验证一下。


好,准备发车。


小心求证

关于 mybatis 我之前写过这篇文章《很开心,在使用mybatis的过程中我踩到一个坑》,其中提到了一个逆向排查法。有兴趣的可以去看一下。


在这篇文章中我们还是来个常规分析吧。本文分析源码为 mybatis 3.4.0 版本。


首先,我先问你一个问题。SpringBoot 是怎么加载 mybatis 的?



熟悉 SpringBoot 启动过程的朋友知道,SpringBoot 会去加载mybatis-spring-boot-autoconfigure-x.x.x.jar下 META-INF 中的spring.factories文件:


所以,下面的 sqlSessionFactory 方法就是我们的入口处:


入口给你找到了,你可以直接在这里加上断点开始 debug 了。


我知道,虽然是刚刚开始,但是可能有些读者觉得已经超纲了。但是没有关系的,继续看下去,我这里只是给你说个入口在哪而已。



由于 debug 的过程不是文本重点,这里就不去介绍了。debug 的时候我们会看到这个方法:

org.apache.ibatis.builder.xml.XMLMapperBuilder#parse


这个方法的第 92 行,就是我们的 xml 内容:


然后在下面这个方法中对 xml 文件进行疯狂的解析:

org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode


图片可以点开看大图哦,debug 模式,可以看到一些输出:


上面的源码的第 94 行,获 取 SqlSource 很关键,要好好看看,这里调用了这个方法:

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configurationorg.apache.ibatis.parsing.XNodejava.lang.Class<?>)


接着在下面方法的第 52 行,剥离出整个完整的 sql:

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode



上面就是常规的 xml 形式的 SQL 原始语句(变量、条件表达式都还未进行替换,不可直接执行的 SQL)获取过程,不是本文重点,简单的分析一下就行。



接下来继续 debug 的时候会遇到下面这个方法,看包名你就知道,这就是我们关心的注解解析相关的方法了:

org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse


在这个方法里面,会去循环处理 mapper 类中的方法:


接下来,就会遇到这个方法了:

org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#getSqlSourceFromAnnotations


当循环到 annotationQueryAgeByName 方法的时候,下面方法的一些关键参数如下所示:


首先我们看 428 行,解析到了 sqlAnnotationType 为 Select:


所以会进入下面的 if 分支,然后运行到 435 行,通过反射获取到了 @Select 注解上的 SQL 语句:


继续往下走,通过 436 行,我们可以走到这个方法:

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, java.lang.String, java.lang.Class<?>)


这个方法就有点意思了,进来判断了 script 即 SQL 是否是以 <script> 开头的,如果是,则走的和之前 xml 一样的解析逻辑:


我第一次看到这个地方的时候,一下才恍然大悟过来,我才明白,@Select 的本质还是 xml 文件的形式啊。只是换了个展现形式而已。


我之前的一个问题,或者说是错误的看法也就迎刃而解了。


我之前认为 @Select 的方式是只能支持简单 SQL 的书写,对于一些类似于判空的需求是不支持的。(因为对 mybatis 注解开发确实不熟)


比如在 xml 文件中这样去写:

<when test='startPage !=null and pageSize != null '>
  LIMIT #{startPage},#{pageSize}
</when>


用 @Select 注解时,只是需要用 <script> 标签包裹起来即可:

@Select("<script>"
    + "SELECT "
     + " age "
    + "FROM user_info"
    + "<if test='startPage !=null and pageSize != null '>"
    + "LIMIT #{startPage},#{pageSize}"
    + "</if>"
    + "</script>")
public List<Intger> getUserInfo(@Param("startPage") Integer startPage,
                @Param("pageSize") Integer pageSize);


只是这个写法,呃,怎么说呢,非常不优雅。


不要为了注解而注解,很明显,这种情况直接用 xml 形式更好。


到这里,我们也知道了,基于 @Select 注解的方式开发时, mybatis 会通过反射获取到注解里面的 SQL ,而这些 SQL 需要一些比较复杂功能,比如判断条件是否为空时,可以用 <script> 标签包裹起来。写法和在 xml 里面开发是一样的。


接下来,我们看看 @SelectProvider 方法是什么个样式。


还是在同样的方法中,只是走向了另外一个分支:


此时的 sqlProviderAnnotation 里面的东西如下:


接着去 new ProviderSqlSource 对象:


在这个方法中,获取到了注解上的具体的提供 SQL 原始语句的方法。


注意红框中框起来的 providerMethod 对象,后面获取真正执行的 SQL 语句的时候还会用到。


同时,我们可以看到 ProviderSqlSource 是 SqlSource 的实现类。


所以,不管是 xml 还是注解,最终都需要获取到一个 SqlSource 对象。


而在本文的示例代码中, xml 和 @Select 生成的是 RawSqlSource。


@SelectProvider 生成的是 ProviderSqlSource。他们里面放的东西是不一样的。


在 RawSqlSource 里面的 sqlSource 变量(类型 StaticSqlSource)放的已经是从 xml 或者 @Select 注解中获取到的 SQL 原始语句了(但是里面的变量还没替换,因为程序启动过程中根本不知道变量的值具体是什么,如果有一些条件表达式的话同理)。


而在ProviderSqlSource 里面,我们前面已经说了,放的是 @SelectProvider 注解上具体的提供 SQL 语句的方法,仅仅是方法,而不是语句。


前面的所有分析都是在我们的方法真正执行之前,接下来,才会 debug 到我们的测试用例,因为只有我们的测试用例里面才有真正的入参, mybatis 才能根据入参,执行最终的 SQL 语句。



所以,接下来,我们就是要找到真正生成 SQL 语句的地方,这里就能和之前文章《很开心,在使用mybatis的过程中我踩到一个坑》中的逆向排查法中得出的结论进行呼应了。


进入 getBoundSql 我们可以看到第292行,就是通过 sqlSource 的 getBoundSql 方法获取到的 boundSql 对象:

org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql



这不就又呼应上了吗?又看到 sqlSource 了。


所以,接下来,我们看一下这两个方法就可以了:

org.apache.ibatis.builder.StaticSqlSource#getBoundSqlorg.apache.ibatis.builder.annotation.ProviderSqlSource#getBoundSql


首先看一下 StaticSqlSource 的实现:


里面的一些关键参数如下:


首先可以 sql 变量,里面是一条待加工的 SQL 语句,我们前面已经分析过了,程序启动的过程中,这里为什么不替换呢?


因为不知道换成啥呀。


那你觉得在这个地方会替换吗?


还是不会的。虽然我们已经告诉 mybatis , userName 就是 why 了,但如果在这个地方把 why 带到 SQL 里面去,我们倒是可以获得一个完整的正确的 SQL。


但是,如果我们传入的是 “why or 1=1”呢?


这是什么东西我相信你一下就恍然大悟了吧,SQL 注入呀。


另外插一句,如果想看 SQL 注入的情况,就是走到 DynamicSqlSource 的情况,在 xml 中把 # 换成 $ 就行,有兴趣的可以试一试。


我这里只是给你截个图,瞅一眼:


好了,我们接着刚才继续说。


继续 debug 会走到这方法中去:

org.apache.ibatis.executor.SimpleExecutor#doQuery


而这个方法的第 62 行,prepareStatement,这个东西不用说了吧,从学 JDBC 的时候就用上它了,老朋友了:


最后去执行真正的查询操作,处理返回值。


接着看 ProviderSqlSource 的实现,注意看我圈起来的那部分的分支判断:


无非就是判断有几个参数,反射方法调用的时候需要怎么传参而已。最终会调用到这个方法里面来获取 SQL 语句:



可以看一下这个时候 providerMethod 和 sql 变量分别是什么:


而这里这个 providerMethod 怎么来的知道了吧?我们前面刚刚分析过了。


new ProviderSqlSource 对象的时候,我还专门说了:“注意红框中框起来的 providerMethod 对象,后面获取真正执行的 SQL 语句的时候还会用到。”


就是在这个地方用到的。


你看,又呼应上了。


这个时候,我们获取到了原始的 SQL 语句了,也有参数了,这样的场景和我们刚刚分析的情况就一模一样了,所以后面的逻辑都一样,进行了代码复用:


进入第 98 行,也就是下面这个我们之前分析过的方法:

org.apache.ibatis.builder.SqlSourceBuilder#parse


在这个方法中,返回了一个 StaticSqlSource 对象:


再次呼应,流程是一样一样的。

另外,再说一下,用 @SelectProvider 注解时的 class 对象里面的方法还可以这样去写,有兴趣的可以去研究一下:


好了,我们的论证部分就算是完了,我发现这个东西,用视频真的几分钟就讲清楚了,描述起来还是有点困难的,难道是在逼我当UP主吗?


不知道大家看的是否明白了,如果对 mybatis 了解不多的朋友可能看起来有一点吃力,但是没有关系,你就把这篇文章当做一个导读,然后自己搞个 Demo 跑起来,玩一玩就行。


个人思考

其实在写这篇文章的时候我就产生了一个思考。


mybatis 为什么要去支持注解呢?


当然,我们都知道,基于注解开发是趋势,给我们简化了非常多的东西。


特别是 SpringBoot 的出现,可以说是注解开发的黄金时代。


遥想当年刚刚入行的时候,开发一个 SSM 项目大多数时间都是在进行 xml 文件的配置。


可以说是很羡慕现在入行的小年轻了,没有真正经历(也许自己搭建过,玩了一下)过被 xml 配置支配的恐惧。


在 xml 时代,大家都是粘来粘去的。而现在基于注解开发了,很多东西都简化了,渐渐的自己也能很轻松的搭建一个可以跑起来的小项目了。


所以,基于注解开发大体上一件很优雅,很好,很值得推广的事情。


为什么说大体上呢?


因为我个人偏见的觉得对于 mybatis 框架来说,没有 xml 文件的 mybatis 是没有灵魂的。


当然,如果你全是简单的 SQL 语句就能实现的功能,你可以用注解开发。但是这个情况,我觉得还是在少数的。


同样,我们可以用注解的形式实现所有 xml 文件能实现的功能。但是我觉得不太优雅。


所以,我觉得一个比较折中的方式是简单 SQL 可以用注解开发,如果是一些有诸如条件判断类的需求的 SQL 还是要写在 xml 文件中。


不要为了拥抱注解,而完全摒弃了 xml 的形式。


你记得吗,在 xml 时代转向注解时代的时候,还有一个经常用到的注解。


有人说这是过渡时代的产物,而在我看来,这更是求同存异的完美体现。


这个注解,就完全的体现最近这句很火的话:


君子美美与共,和而不同。


当然这些都是我在写这篇文章的过程中产生的一些浅显的个人看法而已。不具备参考意义。


骚代码

另外,再给大家分享一个我认为的 mybatis 的骚代码吧。


代码非常的简单明了,很久以前第一次看 mybatis 源码的时候我就是觉得有点“骚”,给我留下了深刻的影响:

org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)


selectOne 方法:


该方法调用的还是 selectList 方法,但是对返回集合进行了一个判断,如果集合大小为 1,说明就真的是 selectOne ,如果大于 1,则抛出异常。


说真的,如果让我去实现这个功能,我不会一下就想到这个方法,我会去老老实实的写功能,然后对返回值进行判断。写完之后,我可能才会发现。哎,这段代码和 selectList 方法可以复用哦,然后才提取出来,变成这样。


记得很久之前面试,面试官问我对看过的源码中哪段影响深刻的,其中我就说到了这个方法。


总之,我个人觉得很妙。

注意坑

然后再说一个之前踩过的坑吧,还导致了一次紧急上线。


还是拿文中的示例说明:


如果我们把返回值从 Integer 变成 int:


用这个测试用例还是会正常查询出结果:


但是,如果我们查询一个数据库中不存在的人的年龄呢?比如这样:


那么就会抛出这样的错误:


找对对应源码,我们可以看到:

当返回值是 null 的时候,但是方法上的返回值类型又不是包装类型中的一种,也不是 void 类型,则抛出异常。


看一下这个方法,是 native 的:

java.lang.Class#isPrimitive



你想想为什么 mybatis 给你进行了这样的一个判断呢?


那就是如果返回为 null ,自动拆箱的时候会抛出空指针的。


即使 mybatis 帮我们挡了一下,我还是完美的踩了一个坑,写出了空指针异常。


代码是这样的,接收的时候我还是用 Integer 去接收了:


但是接口调用方我手贱写成了这样:


明白了吧,妥妥的,空指针,没得跑了。


最后说一句(求关注)

人的潜力真的是巨大的,不逼自己一把,我都不知道我这周真的可以把文章写完。


最近这周晚上回来的时间晚一点了,所以周内的晚上一般都没有时间写文章。


最近成都也热起来了,夏天我有时还是需要一小会的午睡,来保证下午的工作状态的。其他的季节我不需要午休,所以现在中午也很少有时间写一会文章了。


算了一下,连上这篇,我已经连续周更 39 周,输出 48 篇原创文章了。


啊,我真是牛皮呀。


周更作者的痛苦与快乐,你想象不到:


点个“在看”吧,周更很累的,不要白嫖我,需要一点正反馈。


才疏学浅,难免会有纰漏,如果你发现了错误的地方,由于本号没有留言功能,还请你在后台留言指出来,我对其加以修改。


感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。


我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。


欢迎关注我的公众号【why技术】,这号主要进行一些技术分享:


同时也可以关注我的另外一个号【why不止技术】哦,这个号主要负责荒腔走板聊生活、品人生、写书评、谈电影的部分:

往期回顾

我告诉你这书的第 3 版到底值不值得买?


每天都在用,但你知道 Tomcat 的线程池有多努力吗?


如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。


有读者叫我把这个放在赞赏之后,结果放不了。

但我还是非常感动,感谢读者提出的各种建议。

点亮"在看",别白嫖我,好吗?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存