Mybatis系列10:代理模式在延迟加载中的应用

延迟加载时代理的典型应用之一,在分析Spring代码时,我们梳理了反射和代理模式模式的原理,这里,我们重点看CGLIB和JAVASSIST的用法和如何落地的。
“延迟加载”的含义是:暂时不用的对象不会真正载入到内存中, 直到真正需要使用该对象时,才去执行数据库查询操作,将该对象加载到内存中 。在 MyBatis 中,如果一个对象的某个属性需要延迟加载,那么在映射该属性时,会为该属性创建相应的代理对象并返回; 当真正要使用延迟加载的属性时,会通过代理对象执行数据库加载操作,得到真正的数据。所以我们就看一下代理模式,以及如何落地的。
一个属性是否能够延时加载,主要看两个地方的配置:
(1)如果属性在<resultMap>中的相应节点明确地配置了 fetchType 属性,则按照 fetchType属性决定是否延迟加载。
(2)如果未配 置 fetchType 属性,则需要根据 mybatis-config.xml 配置文件中的lazyLoadingEnabled 配置决定是否延时加载,具体配置如下 :

<setting name= ” lazyLoadingEnabled” value= "true ” />
 <setting name="aggressiveLazyLoading” value=” false " />

与延时加载相关的另一个配置项是 aggressiveLazyLoading , 当该配置项为 true 时,表示有延迟加载属性的对象在被调用,将完全加载其属性,否则属性将按需要加载属性 。
MyBatis 中的延迟加载是通过动态代理实现的,可能读者第一反应就是使用前面介绍的 JDK动态代理实现该功能。但是正如前面的介绍所述,要使用 JDK 动态代理的方式为一个对象生成代理对象, 要求该目标类必须实现了(任意〉接口,而 MyBatis 映射的结果对象大多是普通的JavaBean , 并没有实现任何接口,所以无法使用 JDK 动态代理。 MyBatis 中提供了另外两种可以为普通 JavaBean 动态生成代理对象的方式,分别是 CGLIB 方式和 JAVASSIST 方式 。

1.CGlib

cglib 采用字节码技术实现动态代理功能,其原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。因为 cglib使用生成子类的方式实现动态代理,所以无法代理 final 关键宇修饰的方法。 cglib 与 JDK 动态代理之间可以相互补充 : 在目标类实现接口时 ,使用 JDK 动态代理创建代理对象,但当目标类没有实现接口时,使用 cglib 实现动态代理的功能。在 Spring 、 MyBatis 等多种开源框架中,都可以看到 JDK 动态代理与 cglib 结合使用的场景。
下面通过一个示例简单介绍 cglib 的使用 。在使用 cglib 创建动态代理类时,首先需要定义
一个 Callback 接口的实现, cglib 中也提供了 多个 Callback 接口的子接口:
在这里插入图片描述
MethodInterceptor是Callback的子接口,我们基于该接口实现一个CGlib的例子:
代理类:

public class CglibProxy implements MethodInterceptor {

  private Enhancer enhancer = new Enhancer(); // cglib中的Enhancer对象

  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("前置处理");
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("后置处理");
    return result;
  }

  public Object getProxy(Class clazz) {
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(this);
    return enhancer.create();
  }
  
}

测试类:

public class CGLibTest {
  public String method(String str) {
    System.out.println("CGLib method():" + str);
    return str;
  }

  public static void main(String[] args) {
    CglibProxy proxy = new CglibProxy();
    CGLibTest proxyImp = (CGLibTest) proxy.getProxy(CGLibTest.class);
    String result = proxyImp.method("test");
    System.out.println(result);
  }
}

打印结果
在这里插入图片描述

2.Javassist

Javassist 是一个开源的生成 Java 字节码的类库,其主要优点在于简单、快速,直接使用
Javassist 提供的 JavaAPI 就能动态修改类的结构,或是动态的生成类。
Javassist 的使用 比较简单,首先来看如何使用 Javassist 提供的 Java API 动态创建类

代理接口:

/**
 * 可以唱歌的
 */
public interface Singable {
  /**
   * 唱歌
   */
  void sing();
}

被代理类:

/**
 * 歌手
 */
public class Singer implements Singable {
  @Override
  public void sing() {
    System.out.println("I am singing...");
  }
}

创建代理接口:

public class Client {

  public static void main(String[] args) throws Exception {
    Singable proxy = createJavassistDynamicProxy();
    proxy.sing();
  }

  private static Singable createJavassistDynamicProxy()
      throws Exception {
    ProxyFactory proxyFactory = new ProxyFactory();
// 设置实现的接口
    proxyFactory.setInterfaces(new Class[]{Singable.class});
    Class<?> proxyClass = proxyFactory.createClass();
    Singable javassistProxy = (Singable) proxyClass.getDeclaredConstructor().newInstance();
    ((ProxyObject) javassistProxy).setHandler(new JavassistInterceptor(new Singer()));
    return javassistProxy;
  }

  private static class JavassistInterceptor implements MethodHandler {

    // 被代理对象
    private Object delegate;

    private JavassistInterceptor(Object delegate) {
      this.delegate = delegate;
    }

    /**
     * @param self 创建的代理对象
     * @param m 被代理方法
     * @param proceed 如果代理接口,此参数为null,如果代理类,此参数为父类的方法
     * @param args 方法参数
     */
    public Object invoke(Object self, Method m, Method proceed,
        Object[] args) throws Throwable {
      System.out.println("javassist proxy before sing");
      Object ret = m.invoke(delegate, args);
      System.out.println("javassist proxy after sing");
      return ret;
    }
  }
}

和JDK的动态代理创建方式类似,但Javassist也可以代理类。

public class Client {

  public static void main(String[] args) throws Exception {
    Singable proxy = createJavassistDynamicProxy();
    proxy.sing();
  }

  private static Singable createJavassistDynamicProxy()
      throws Exception {
    ProxyFactory proxyFactory = new ProxyFactory();
// 设置父类
    proxyFactory.setSuperclass(Singer.class);
    Class<?> proxyClass = proxyFactory.createClass();
    Singable javassistProxy = (Singable) proxyClass.getDeclaredConstructor().newInstance();
    ((ProxyObject) javassistProxy).setHandler(new JavassistInterceptor());
    return javassistProxy;
  }

  private static class JavassistInterceptor implements MethodHandler {

    /**
     * @param self 创建的代理对象
     * @param m 被代理方法
     * @param proceed 如果代理接口,此参数为null,如果代理类,此参数为父类的方法
     * @param args 方法参数
     */
    public Object invoke(Object self, Method m, Method proceed,
        Object[] args) throws Throwable {
      System.out.println("javassist proxy before sing");
// 调用父类的sing方法
      Object ret = proceed.invoke(self, args);
      System.out.println("javassist proxy after sing");
      return ret;
    }
  }
}

还可以使用Javassist提供的字节码API实现:

public class Client {

  public static void main(String[] args) throws Exception {
    Singable proxy = createJavassistBytecodeDynamicProxy(new Singer());
    proxy.sing();
  }

  private static Singable createJavassistBytecodeDynamicProxy(Singable delegate) throws Exception {
    ClassPool mPool = new ClassPool(true);
    CtClass mCtc = mPool.makeClass(Singable.class.getName() + "JavaassistProxy");
    mCtc.addInterface(mPool.get(Singable.class.getName()));
    mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
    mCtc.addField(CtField.make("public " + Singable.class.getName() + " delegate;", mCtc));
    String src = "public void sing() { "
        + "System.out.println(\"javassist bytecode proxy before sing\");"
        + "delegate.sing();"
        + "System.out.println(\"javassist bytecode proxy after sing\"); "
        + "}";
    mCtc.addMethod(CtNewMethod.make(src, mCtc));
    Class<?> pc = mCtc.toClass();
    Singable bytecodeProxy = (Singable) pc.getDeclaredConstructor().newInstance();
    Field filed = bytecodeProxy.getClass().getField("delegate");
    filed.set(bytecodeProxy, delegate);
    return bytecodeProxy;
  }

}


Javassist可以直接拼接java源码生成字节码,这是比ASM易用的地方,但也会造成一定的性能损失。

3.延迟加载实现的过程

MyBatis 中与延迟加载相关的类有 ResultLoader、 ResultLoaderMap 、 ProxyFactoy接口及实现类。ResultLoader 主要负责保存一次延迟加载操作所需的全部信息,ResultLoader 的核心是 loadResult()方法,该方法会通过 Executor 执行ResultLoader中记录的SQL语句井返回相应的延迟加载对象。

  public Object loadResult() throws SQLException {
    List<Object> list = selectList();
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
  }

其中, selectList()方法才是完成延迟加载操作的地方,具体实现如下:

  private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
      localExecutor = newExecutor();
    }
    try {
      return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
    } finally {
      if (localExecutor != executor) {
        localExecutor.close(false);
      }
    }
  }

延迟加载得到的是 List类型的对象 , ResultEx位actor.extractObjectFromList()方法负责将其转换为 targetType 类型的对象,大致逻辑如下:

  • 如果目标对象类型为 List,则无须转换。
  • 如果目标对象类型是 Collection 子类、数组类型(其中项可以是基本类型 ,也可以是对象类型),则创建 targetType 类型的集合对象,并复制 List<O均ect>中的项。
  • 如果目标对象是普通 Java 对象且延迟加载得到的 List 大小为 1 ,则认为将其中唯一的项作为转换后的对象返回。
  • ResultLoaderMap 与 ResultLoader 之 间的关系非常密切,在 ResultLoaderMap 中使用 loadMap字段保存对象中延迟加载属性及其对应的 ResultLoader 对象之间的关系。ResultLoaderMap 中提供了 load()和 loadAll()两个执行延迟加载的入口方法,前者负责加载指定名称的属性,后者则是加载该对象中全部的延迟加载属性。ResultLoaderMap.load()方法和 loadAll()方法最终都是通过调用 LoadPair.load()方法实现的,
    LoadPair.load()方法的具体代码如下:
  public boolean load(String property) throws SQLException {
    LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    if (pair != null) {
      pair.load();
      return true;
    }
    return false;
  }

这里的loaderMap存的就是代理对象。javaassist和CGlib都使用该map作为代理对象的缓存。
之后就是javaassist和CGlib分别执行相关的代理逻辑了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值