SQL解析

Mybatis在初始化的时候,会读取xml中的SQL,解析后会生成SqlSource对象,SqlSource对象分为两种。

  • DynamicSqlSource ,动态SQL,获取SQL( getBoundSQL 方法中)的时候生成参数化SQL。

  • RawSqlSource ,原始SQL,创建对象时直接生成参数化SQL。

因为 RawSqlSource 不会重复去生成参数化SQL,调用的时候直接传入参数并执行,而 DynamicSqlSource 则是每次执行的时候参数化SQL,所以 RawSqlSource 是 DynamicSqlSource 的性能要好的。

解析的时候会先解析 include 标签和 selectkey 标签,然后判断是否是动态SQL,判断取决于以下两个条件:

  • SQL中有动态拼接字符串,简单来说就是是否使用了 ${} 表达式。注意这种方式存在SQL注入,谨慎使用。
  • SQL中有 trim 、 where 、 set 、 foreach 、 if 、 choose 、 when 、 otherwise 、 bind 标签

相关代码如下:

通过源代码分析Mybatis的功能

参数解析

Mybais中用于解析Mapper方法的参数的类是 ParamNameResolver ,它主要做了这些事情:

  • 每个Mapper方法第一次运行时会去创建 ParamNameResolver ,之后会缓存

  • 创建时会根据方法签名,解析出参数名,解析的规则顺序是

    1. 如果参数类型是 RowBounds 或者 ResultHandler 类型或者他们的子类,则不处理。

    2. 如果参数中有 Param 注解,则使用 Param 中的值作为参数名

    3. 如果配置项 useActualParamName =true, argn (n>=0) 标作为参数名,如果你是Java8以上并且开启了 -parameters`,则是实际的参数名

      如果配置项 useActualParamName =false,则使用 n (n>=0)作为参数名

相关源代码:

通过源代码分析Mybatis的功能

而在使用这个 names 构建xml中参数对象和值的映射时,还进行了进一步的处理。

通过源代码分析Mybatis的功能

另外值得一提的是,对于集合类型,最后还有一个特殊处理

通过源代码分析Mybatis的功能

由此我们可以得出使用参数的结论:

  • 如果参数加了 @Param 注解,则使用注解的值作为参数
  • 如果只有一个参数,并且不是集合类型和数组,且没有加注解,则使用对象的属性名作为参数
  • 如果只有一个参数,并且是集合类型,则使用 collection 参数,如果是 List 对象,可以额外使用 list 参数。
  • 如果只有一个参数,并且是数组,则可以使用 array 参数
  • 如果有多个参数,没有加 @Param 注解的可以使用 argn 或者 n (n>=0,取决于 useActualParamName 配置项)作为参数,加了注解的使用注解的值。
  • 如果有多个参数,任意参数只要不是和 @Param 中的值覆盖,都可以使用 paramn (n>=1)

延迟加载

Mybatis是支持延迟加载的,具体的实现方式根据 resultMap 创建返回对象时,发现fetchType=“lazy”,则使用代理对象,默认使用 Javassist (MyBatis 3.3 以上,可以修改为使用 CgLib )。代码处理逻辑在处理返回结果集时,具体代码调用关系如下:

PreparedStatementHandler.query => handleResultSets => handleResultSet => handleRowValues => handleRowValuesForNestedResultMap => getRowValue

getRowValue 中,有一个方法 createResultObject 创建返回对象,其中的关键代码创建了代理对象:

通过源代码分析Mybatis的功能

另一方面, getRowValue 会调用 applyPropertyMappings 方法,其内部会调用 getPropertyMappingValue ,继续追踪到 getNestedQueryMappingValue 方法,在这里,有几行关键代码:

通过源代码分析Mybatis的功能

这几行的目的是跳过属性值的加载,等真正需要值的时候,再获取值。

Executor

Executor是一个接口,其直接实现的类是 BaseExecutor 和 CachingExecutor , BaseExecutor又派生了 BatchExecutor 、 ReuseExecutor 、 SimpleExecutor 、 ClosedExecutor 。其继承结构如图:

其中 ClosedExecutor 是一个私有类,用户不直接使用它。

  • BaseExecutor :模板类,里面有各个Executor的公用的方法。
  • SimpleExecutor :最常用的 Executor ,默认是使用它去连接数据库,执行SQL语句,没有特殊行为。
  • ReuseExecutor :SQL语句执行后会进行缓存,不会关闭 Statement ,下次执行时会复用,缓存的 key 值是 BoundSql 解析后SQL,清空缓存使用 doFlushStatements 。其他与 SimpleExecutor 相同。
  • BatchExecutor :当有 连续 的 Insert 、 Update 、 Delete 的操作语句,并且语句的 BoundSql 相同,则这些语句会批量执行。使用 doFlushStatements 方法获取批量操作的返回值。
  • CachingExecutor :当你开启二级缓存的时候,会使用 CachingExecutor 装饰 SimpleExecutor 、 ReuseExecutor 和 BatchExecutor ,Mybatis通过 CachingExecutor 来实现二级缓存。

缓存

一级缓存

Mybatis一级缓存的实现主要是在 BaseExecutor 中,在它的查询方法里,会优先查询缓存中的值,如果不存在,再查询数据库,查询部分的代码如下,关键代码在17-24行:

通过源代码分析Mybatis的功能

而在 queryFromDatabase 中,则会将查询出来的结果放到缓存中。

通过源代码分析Mybatis的功能

而一级缓存的Key,从方法的参数可以看出,与调用方法、参数、rowBounds分页参数、最终生成的sql有关。

通过源代码分析Mybatis的功能

通过查看一级缓存类的实现,可以看出一级缓存是通过HashMap结构存储的:

通过源代码分析Mybatis的功能

通过配置项,我们可以控制一级缓存的使用范围,默认是Session级别的,也就是SqlSession的范围内有效。也可以配制成Statement级别,当本次查询结束后立即清除缓存。

当进行插入、更新、删除操作时,也会在执行SQL之前清空以及缓存。

二级缓存

Mybatis二级缓存的实现是依靠 CachingExecutor 装饰其他的 Executor 实现。原理是在查询的时候先根据CacheKey查询缓存中是否存在值,如果存在则返回缓存的值,没有则查询数据库。

CachingExecutor 中 query 方法中,就有缓存的使用:

通过源代码分析Mybatis的功能

那么这个 Cache 是在哪里创建的呢?通过调用的追溯,可以找到它的创建:

通过源代码分析Mybatis的功能

从方法的第一行可以看出,Cache对象的范围是namespace,同一个namespace下的所有mapper方法共享Cache对象,也就是说,共享这个缓存。

另一个创建方法是通过CacheRef里面的:

通过源代码分析Mybatis的功能

这里的话会通过 CacheRef 中的参数 namespace ,找到那个 Cache 对象,且这里使用了 unresolvedCacheRef ,因为Mapper文件的加载是有顺序的,可能当前加载时引用的那个 namespace 的Mapper文件还没有加载,所以用这个标记一下,延后加载。

二级缓存通过 TransactionalCache 来管理,内部使用的是一个HashMap。Key是Cache对象,默认的实现是 PerpetualCache ,一个namespace下共享这个对象。Value是另一个Cache的对象,默认实现是 TransactionalCache ,是前面那个Key值的装饰器,扩展了事务方面的功能。

通过查看 TransactionalCache 的源码我们可以知道,默认查询后添加的缓存保存在待提交对象里。

通过源代码分析Mybatis的功能

只有等到 commit 的时候才会去刷入缓存。

通过源代码分析Mybatis的功能

查看 clear 代码,只是做了标记,并没有真正释放对象。在查询时根据标记直接返回空,在 commit 才真正释放对象:

通过源代码分析Mybatis的功能

rollback 会清空这些临时缓存:

通过源代码分析Mybatis的功能

根据二级缓存代码可以看出,二级缓存是基于 namespace 的,可以跨SqlSession。也正是因为基于 namespace ,如果在不同的 namespace 中修改了同一个表的数据,会导致脏读的问题。

插件

Mybatis的插件是通过代理对象实现的,可以代理的对象有:

  • Executor :执行器,执行器是执行过程中第一个代理对象,它内部调用 StatementHandler返回SQL结果。
  • StatementHandler :语句处理器,执行SQL前调用 ParameterHandler 处理参数,执行SQL后调用 ResultSetHandler 处理返回结果
  • ParameterHandler :参数处理器
  • ResultSetHandler :返回对象处理器

这四个对象的接口的所有方法都可以用插件拦截。

插件的实现代码如下:

通过源代码分析Mybatis的功能

可以很明显的看到,四个方法内都有 interceptorChain.pluginAll() 方法的调用,继续查看这个方法:

通过源代码分析Mybatis的功能

这个方法比较简单,就是遍历 interceptors 列表,然后调用器 plugin 方法。 interceptors是在解析XML配置文件是通过反射创建的,而创建后会立即调用 setProperties 方法

我们通常配置插件时,会在 interceptor.plugin 调用 Plugin.wrap ,这里面通过Java的动态代理,拦截方法的实现:

通过源代码分析Mybatis的功能

而拦截的参数传了 Plugin 对象,Plugin本身是实现了 InvocationHandler 接口,其 invoke方法里面调用了 interceptor.intercept ,这个方法就是我们实现拦截处理的地方。

注意到里面有个 getSignatureMap 方法,这个方法实现的是查找我们自定义拦截器的注解,通过注解确定哪些方法需要被拦截:

通过源代码分析Mybatis的功能

通过源代码我们可以知道,创建一个插件需要做以下事情:

  1. 创建一个类,实现 Interceptor 接口。
  2. 这个类必须使用 @Intercepts 、 @Signature 来表明要拦截哪个对象的哪些方法。
  3. 这个类的 plugin 方法中调用 Plugin.wrap(target, this) 。
  4. (可选)这个类的 setProperties 方法设置一些参数。
  5. XML中 <plugins> 节点配置 <plugin interceptor="你的自定义类的全名称"></plugin> 。

 

相关文章:

  • 2021-05-20
  • 2021-10-11
  • 2022-03-04
  • 2021-12-27
  • 2021-12-29
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2022-01-15
  • 2021-07-02
  • 2022-01-14
  • 2021-09-20
相关资源
相似解决方案