Mybatis Lazy Loading(懶加載) 實現原理

在上一篇Mybatis查詢邏輯時候,有一個點就是懶加載,這個點其實有點複雜,所以博主單獨拿出來分析。

本文從以下角度展開:

  1. 什麼是懶加載?
  2. Mybatis對懶加載的配置如何?
  3. 懶加載通過什麼方式實現懶加載的?

懶加載使用

  1. 配置中加入以下兩行:
        <!-- 打開延遲加載的開關 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- 將積極加載改爲消極加載(及按需加載) -->
        <setting name="aggressiveLazyLoading" value="false" />
  1. 在所需要的resultMap 的 列上加上 fetchType="lazy" 表明是懶加載
    例如:
  <resultMap id="LazyResultMap" type="anla.learn.mybatis.interceptor.model.UserLazyDepartment" >
    <id column="uid" property="uid" jdbcType="INTEGER" />
    <result column="email" property="email" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="department_id" property="departmentId" jdbcType="BIGINT"/>
    <!-- 懶加載 -->
    <association property="department" fetchType="lazy" select="anla.learn.mybatis.interceptor.dao.DepartmentMapper.getByIndex" javaType="anla.learn.mybatis.interceptor.model.Department" column="department_id"/>
  </resultMap>

具體看一看例子:
https://github.com/anLA7856/mybatislearn/blob/master/mybatis-interceptor/src/test/java/MybatisTest.java

分析

從結果入手,使用懶加載和不使用懶加載的返回的對象有什麼區別呢?

  1. 正常查詢:
    在這裏插入圖片描述
  2. 定義懶加載查詢:
    在這裏插入圖片描述

兩幅圖片比較明顯,懶加載返回對象不是原本的對象類型,而是帶有後綴的字節碼動態生成的類。

那麼現在的目的就是找出懶加載爲返回對象爲何是動態生成字節碼類?

ResultSet處理

配置懶加載是,在<resultMap>增加<association> 節點配置,那麼是否爲處理結果發現了 fetchType=lazy 的配置,從而動態生成了類,從而當返回對象調用某些方法時,執行 懶加載查詢語句呢?
efaultResultSetHandlerhandlerResultSet 開始,而後往下一步步 調試

  1. handleResultSet(rsw, resultMap, multipleResults, null);
  2. handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
  3. handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
  4. Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
  5. Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);

最終要返回的object 對象是由createResultObject 方法生成:

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // 首先創建一個正常的返回對象
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        // 判斷是否懶加載
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

上面邏輯可以看出,默認會生成一個 正常的對應 Type 的 resultObject,而當判斷有嵌套查詢或者有懶加載變量時,則會對已有的 resultObject 重新賦值:
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
看看這個懶加載的代理生成過程:

  @Override
  public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  // 創建代理
    return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
  }
    public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    // 獲取返回類型
      final Class<?> type = target.getClass();
      // 構造callback,這個是精髓
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      // 使用Javassist創建代理
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      // 拷貝屬性
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

上面代碼可以使用以下邏輯:

  1. 構造 代理 callback 即 EnhancedResultObjectProxyImpl
  2. crateProxy(type, callback, constructorArgTypes, constructorArgs); 爲使用 javassist 創建。
  3. PropertyCopier.copyBeanProperties(type, target, enhanced); 爲將 target 屬性拷貝到enhanced中。

看看 EnhancedResultObjectProxyImpl
它實現了 javassist.util.proxy.MethodHandler, 所以實際上返回對象每次調用方法,都會調用 EnhancedResultObjectProxyImpl 的invoke方法,也就是說每個方法都被攔截了:
invoke:

    @Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
        // 對 lazyLoader 加鎖
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
          // 如果是 writeReplace 方法
            Object original;
            if (constructorArgTypes.isEmpty()) {
            // 創建對象
              original = objectFactory.create(type);
            } else {
            // 使用構造器創建對象
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
            // 如果仍然有爲執行懶加載,則需要適配
              return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
            // 否則直接返回創建成的對象
              return original;
            }
          } else {
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
            // 如果有懶加載並且執行的方法不爲 finalize
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
              // 如果 aggressiveLazyLoading 爲true 或者 包含 "equals", "clone", "hashCode", "toString" 之一,則全部加載
                lazyLoader.loadAll();
              } else if (PropertyNamer.isSetter(methodName)) {
              // 如果是setter 方法,那麼清楚懶加載map
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
              } else if (PropertyNamer.isGetter(methodName)) {
              // 如果是get,那麼判斷是否爲懶加載 所需loader ,是就執行懶加載
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        // 執行真正方法
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }

上面 代碼有以下邏輯:

  1. 對懶加載 map 加鎖
  2. 判斷是否爲 writeReplace 序列化用方法,是的話在序列化成文件時候,需要判斷是否加入 懶加載map集合對象。
  3. 判斷當前對象 lazyLoader 是否有值,即是否有未完成的懶加載查詢。lazyLoader 是在 getRowValue中賦值的,也就是說在創建代理對象時 lazyLoader 爲空,在後面給代理對象每個字段進行屬性賦值時,會判斷是否爲懶加載(lazy loading),從而設置lazyLoader的值。
    lazyLoader 中加載過屬性不會加載第二次,會從 lazyLoader 中刪除,所以不用擔心每次都會重新查詢。
  4. 判斷是否配置了 aggressiveLazyLoadingtrue, aggressiveLazyLoading 爲true則默認觸發一個懶加載時會將所有都加載出來,或者 包含 “equals”, “clone”, “hashCode”, “toString” 之一,則全部加載。
  5. 如果是setXxx方法,則會不進行懶加載操作,並且將 lazyLoader 對應字段刪除。
  6. 如果是getXxx方法,則會從lozyLoader 執行sql加載出來。

ResultLoaderMap 中維護了一個 Map<String, LoadPair>, key 爲屬性,而LoadPair則作爲一個句柄去調用加載的語句 等語句。

最終到 LoadPairload 方法時,實際只會進行 select 操作。
LoadPair 屬於 ResultLoaderMap 的內部類,最終會調用 ResultLoaderloadResult 方法:

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

  private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
    // 如果不是當前線程,則新建執行器
      localExecutor = newExecutor();
    }
    try {
    // 否則使用該執行其執行query方法
      return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
    } finally {
      if (localExecutor != executor) {
        localExecutor.close(false);
      }
    }
  }

loadResult 則是獲取 Executor 而後執行 query 邏輯。

但是有意思的是,懶加載 使用 <association/> 節點,雖然默認行爲是 select 動作,但事實上你可以方任意一個 查詢,可以爲 selectinsertdeleteupdate 等語句,仍然會以懶加載機制,到該執行時候會被執行。

覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,一起研究Mybatis:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章