通过源代码分析Mybatis的功能

64 篇文章 1 订阅
32 篇文章 0 订阅

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 标签

相关代码如下:

参数解析

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

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

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

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

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

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

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

相关源代码:

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

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

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

  • 如果参数加了 @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 创建返回对象,其中的关键代码创建了代理对象:

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

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

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行:

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

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

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

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

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

二级缓存

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

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

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

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

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

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

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

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

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

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

rollback 会清空这些临时缓存:

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

插件

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

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

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

插件的实现代码如下:

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

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

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

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

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

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

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值