【Mybatis源码分析】动态代理的使用(Javassist、CGLIB、JDK动态代理)

先说说这篇博客说得啥?

本是不想写这篇博客的,因为关于 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,最佳~

猜你喜欢

转载自blog.csdn.net/qq_63691275/article/details/132734294
今日推荐