先说说这篇博客说得啥?
本是不想写这篇博客的,因为关于 Mybatis 对 Mapper 的动态代理实现也很简单,就是使用 JDK 动态代理,调用其接口中的方法转到调用到 sqlSession 的方法上去,然后和上一篇的Mybatis查询流程 源码分析串起来就可以了,顶多需要注意点 Mybatis 是如何处理参数的就是。
但是我发现 Mybatis 还引入了 CGLIB 动态代理库,why?这我有以下几个疑问?
- 为什么代理 Mapper 不使用 CGLIB 动态代理?
- 为什么处理映射对象动态代理实现懒加载不使用CGLIB动态代理异或是JDK提供的动态代理,而是引入 javassist 代理库呢?
这篇博客除了源码分析带大家知道 Mybatis 代理 Mapper 是如何处理参数的,还会给大伙解释清楚那俩个问题。
Mybatis 俩处使用了动态代理,一处是动态代理 Mapper 对象,供外界面向抽象操纵数据库.另一处是针对返回值对象进行动态映射实现懒加载。
Mybatis 处理参数源码分析
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
}
if (!hasParamAnnotation && paramCount == 1) {
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(names.firstKey()) : null);
} else {
// 针对没有使用 @Param 注解和含多个参数的
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// key 对应的是参数名,或者说@Param中的字符串,Value是对应args中的值
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// 通用的参数名称,ORM映射需要的名称,需要和填充的相对于
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
它内部封装了个 names,会指示各个参数对应的名字,如果用了 @Param 注解,其对应参数名字就会使用 @Param 中的 Value 属性对应的值。
如果有多个参数或者说用了 @Param 注解的话,它会将 名称-》对应参数值 封装成一个 map 对象然后返回。为了避免没有使用 @Param 注解的参数,它会添加通用的名称对应的参数值,即类似param1、param2.....
然后这边知道怎么把方法中的参数转换成对应后,就可以将前面说的串起来了。
也是 ${} 和 #{} 区别?
在使用执行器准备执行对应 SQL 时——
会调用 MappedStatement.getBoundSql(param) 也就是调用 SqlSource.getBoundSql(param)——
我们所说的${}在解析动态SQL的时候其实它对应的就是 TextSqlNode,在getBoundSql执行中就会把 ${} 替换成对应的参数值,#{} 会用 ? 去替换,这个时候就得到真正的sql片段——
此时的sql片段预编译后就可以获得到PreparedStatement操纵对象——得到它就可以进行上一篇说的执行流程了
为什么实现懒加载用的是Javassist动态代理
首先得分析一下它使用该动态代理的时机。
是在执行查询时,然后判断是否配置了 fetchType=lazy 且存在子查询,如果是的话就使用动态代理去处理返回值对象。查询是随着外界调用而执行的操作,是运行时的,而不是项目加载就会去执行,可以理解为是执行了 Mapper 代理,然后内部执行了对应的查询。
抓住这个时机呢,就知道使用该动态代理是在运行时实现的。
然后 Mybatis 实现的该代理过程是先生成一个代理类,然后该代理类是会去继承那个实际的类,并且去实现对应的接口,实际的代理对象就是这个生成的代理类的对象。也就是说在运行时得生成这个代理类的字节码,那使用 Javassist 相比其他动态代理库来说是很好的选择。
举个例子,假设有一个 User 对象,在启用懒加载的情况下,MyBatis 会动态生成一个 UserProxy 类作为 User 对象的代理。UserProxy 类继承自 User 类,并实现了 User 接口(如果有)。在 UserProxy 类中,会覆写访问延迟加载属性的方法,包括 getter 方法。
为什么Mapper代理用的是JDK自带的动态代理
这个其实很简单,因为咱定义的Mapper是接口,然而JDK自带的解决动态代理的相关api就是处理接口的,那肯定是最直接的方案。至于引入CGLIB依赖,是为了满足用户需求,如果用户想用CGLIB去实现Mapper的动态代理可以进行配置。如下:
<configuration>
<settings>
<setting name="proxyFactory" value="org.apache.ibatis.executor.CglibProxyFactory" />
</settings>
...
</configuration>
所以说引入 CGLIB 依赖是为了让你自由切换咯,注意是生成Mapper代理对象这个代理过程的切换方案。至于懒加载那里就是使用Javassist,最佳~