我第一次看到这个地方的时候,一下才恍然大悟过来,我才明白,@Select 的本质还是 xml 文件的形式啊。只是换了个展现形式而已。
我之前的一个问题,或者说是错误的看法也就迎刃而解了。
我之前认为 @Select 的方式是只能支持简单 SQL 的书写,对于一些类似于判空的需求是不支持的。(因为对 mybatis 注解开发确实不熟)
比如在 xml 文件中这样去写:
<when test='startPage !=null and pageSize != null '> LIMIT #{startPage},#{pageSize} </when>
只是这个写法,呃,怎么说呢,非常不优雅。
不要为了注解而注解,很明显,这种情况直接用 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#getBoundSql org.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 跑起来,玩一玩就行。