Mybatis優雅存取json字段的解決方案 - TypeHandler (二)

回顧

上篇,我們分析了TypeHandler的註冊過程,分析了12個register方法之間盤根錯節的關係,最終得出註冊過程就是構建三個Map的過程。在這個過程中,爲使文章脈絡清晰,跳過了@MappedTypes@MappedJdbcTypes註解相關的內容。跳過並非不重要,相反,這倆註解在自定義TypeHandler的過程中扮演着相當重要的角色,因此,爲使知識點完整,本文將分析@MappedTypes@MappedJdbcTypes註解的使用

正文

@MappedTypes: 用於指定TypeHandler能處理的JavaType

上篇中,我們提到過TypeReference,自定義的TypeHandler一般會間接繼承該類,Mybatis通過該類從泛型中提取JavaType。除此之外,還可以通過@MappedTypes註解直接標明JavaType,而且註解的優先級高於泛型提取方式。也即是說,假若使用了@MappedTypes註解,會忽視泛型中的JavaType

@MappedJdbcTypes: 用於限定TypeHandler能處理的JdbcType

注意二者措詞稍有不同,前者是指定,而後者是限定。前者突出的是:在泛型提取方式的基礎上,允許通過某種方式強行指定能處理的JavaType,而後者突出的是:在原來不加限制的JdbcType上,通過某種方式限定僅允許處理某種JdbcType

舉個例子,自定義的TypeHandler未指定@MappedJdbcTypes,能夠處理多種類型,如VARCHARCHAR等,一旦限定爲@MappedJdbcTypes(JdbcType.VARCHAR),就只能處理VARCHAR。這樣會導致一個奇怪問題:在Mapper.xml使用resultMap定義結果集映射,屬性未指定jdbcTypeMybatis就按照javaType=[TheJavaType], jdbcType=nullTypeHandlerRegistry中找,由於限定了自定義TypeHandlerJdbcTypeVARCHAR,Map裏找不到,因此查找失敗。懶惰是程序員的天性,倘若是手寫的Mapper.xml,很多開發人員並不會主動指定JdbcType,程序就要報錯了。

爲了解決這個問題,@MappedJdbcTypes有一個屬性includeNullJdbcType用於指定是否將null作爲JdbcType進行註冊,默認值是false。我們可以手動指定指爲true,如此,Mybatis按照javaType=[TheJavaType], jdbcType=nullTypeHandlerRegistry中就能找到我們自定義的TypeHandler

更進一步地,爲了讓MyBatis用起來更方便一些,從MyBatis 3.4.0開始,即便未指定includeNullJdbcType=true,對於那些只存在一個TypeHandlerJavaTypeTypeHandler也會生效。再也不用擔心使用@MappedJdbcTypes限定JdbcType之後,忘記配置includeNullJdbcType=true而導致程序啓動失敗。(Since Mybatis 3.4.0 however, if a single TypeHandler is registered to handle a Java type, it will be used by default in ResultMaps using this Java type (i.e. even without includeNullJdbcType=true))

案例

爲了方便,仍然使用上篇的例子

上篇中,定義了抽象基類AbstractObjectTypeHandler,以及能處理JavaType=Bar的具體實現類BarTypeHandler,文中還提到,如果要處理別的JavaType,只要繼承AbstractObjectTypeHandler,並在泛型實現中寫上JavaType即可。雖然這已經是一種很優雅的擴展方式(開閉原則),但是如果存在另一種方式,能夠減少類的膨脹,讓我們在使用之餘,多一種考量方式,豈非更好?請看下述代碼:

@MappedTypes(Bar.class)
public class GenericTypeHandler<T> extends BaseTypeHandler<T> {

    private Class<T> type;

    public GenericTypeHandler(Class<T> type) {
        if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter,
                                    JdbcType jdbcType) throws SQLException {
        ps.setString(i, JsonUtil.toJson(parameter));
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        String data = rs.getString(columnName);
        return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, type);
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String data = rs.getString(columnIndex);
        return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, type);
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        String data = cs.getString(columnIndex);
        return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, type);
    }
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {
  Class<?>[] value();
}

我們定義了一個通用的類型處理器,沒有指定泛型具體實現,而是通過在類上使用@MappedTypes來指定能處理的JavaType,我們指定了Bar.class,說明該類型處理器能夠處理Bar。看一下@MappedTypes的定義,發現value是個class類型的數組,也即是說,除了Bar,還可以繼續在上面追加需要處理的JavaType

如此一來,只需要定義一個類型處理器,通過指定@MappedTypes的方式,就能處理多個JavaType,有效地抑制了類膨脹

如果說@MappedTypes能抑制類膨脹,可以忽視泛型提取,強制指定JavaType方面有作用,那@MappedJdbcTypes就比較雞肋了。它的出現,是爲了限制能支持的JdbcType,但在很多場景中,並不需要限制,只在有限的場景,如同一JavaType在不同的表有不同的字段類型,對應有多個TypeHandler的轉換邏輯,這時候需要通過@MappedJdbcTypes分別指定JdbcType

原理分析

上篇分析12個register方法可以得出如下結論:

  • @MappedTypes註解相關的有2個單參的register方法
  • @MappedJdbcTypes註解相關的有1個雙參的register方法

相關源碼如下:

public void register(Class<?> typeHandlerClass) {
  // 單參,與`@MappedTypes`相關
  boolean mappedTypeFound = false;
  MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> javaTypeClass : mappedTypes.value()) {
      register(javaTypeClass, typeHandlerClass);
      mappedTypeFound = true;
    }
  }
  if (!mappedTypeFound) {
    register(getInstance(null, typeHandlerClass));
  }
}

根據傳進來的typeHandlerClass獲取定義在其上的註解MappedTypes,並提取註解指定的JavaType集合,循環調用register(javaTypeClass, typeHandlerClass)進行註冊,此處源碼可以迴應上面結論:

如此一來,只需要定義一個類型處理器,通過指定@MappedTypes的方式,就能處理多個JavaType

public <T> void register(TypeHandler<T> typeHandler) {
  // 單參,與`@MappedTypes`相關
  boolean mappedTypeFound = false;
  MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> handledType : mappedTypes.value()) {
      register(handledType, typeHandler);
      mappedTypeFound = true;
    }
  }
  // 下半部分在上篇文章已分析,不再贅述
  // @since 3.1.0 - try to auto-discover the mapped type
  if (!mappedTypeFound && typeHandler instanceof TypeReference) {
    try {
      TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
      register(typeReference.getRawType(), typeHandler);
      mappedTypeFound = true;
    } catch (Throwable t) {
      // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
    }
  }
  if (!mappedTypeFound) {
    register((Class<T>) null, typeHandler);
  }
}

此處與前面的方法很像,前面的單參是個typeHandlerClass,而此處是個typeHandler實例,因此多了一步,先獲取typeHandlerClass,再獲取註解在其上的@MappedTypes,後面的步驟也是一樣的,都是爲了註冊通過註解指定的JavaType

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
  // 雙參,與`@MappedJdbcTypes`相關
  MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
  if (mappedJdbcTypes != null) {
    for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
      register(javaType, handledJdbcType, typeHandler);
    }
    if (mappedJdbcTypes.includeNullJdbcType()) {
      register(javaType, null, typeHandler);
    }
  } else {
    register(javaType, null, typeHandler);
  }
}

通過typeHandler實例獲取其class,並拿到註解在類上的@MappedJdbcTypes,並獲取限定的JdbcType集合,調用register(javaType, handledJdbcType, typeHandler)進行註冊。如果註解指定了includeNullJdbcType=true,會將JdbcType=null進行註冊(register(javaType, null, typeHandler)),也就應證了上文:

@MappedJdbcTypes有一個屬性includeNullJdbcType用於指定是否將null作爲JdbcType進行註冊,默認值是false

關於@MappedTypes@MappedJdbcTypes 註解相關的註冊過程,已經分析完畢。可以看到,註解是一個加強功能,它與註冊主流程並不強相關,不存在強依賴關係,只是在TypeHandler註冊過程給開了一個口子,允許強行指定的JavaType或者限定JdbcType。至於註冊後的使用,與上篇闡述的沒有任何區別,因此不再贅述

至此,還剩一個論點未曾分析到:

Since Mybatis 3.4.0 however, if a single TypeHandler is registered to handle a Java type, it will be used by default in ResultMaps using this Java type (i.e. even without includeNullJdbcType=true)

我們來看一下,在源碼上是如何體現的,參考:org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler(java.lang.reflect.Type, org.apache.ibatis.type.JdbcType)


private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  if (ParamMap.class.equals(type)) {
    return null;
  }
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  TypeHandler<?> handler = null;
  if (jdbcHandlerMap != null) {
    handler = jdbcHandlerMap.get(jdbcType);
    if (handler == null) {
      handler = jdbcHandlerMap.get(null);
    }
    if (handler == null) {
      // #591
      handler = pickSoleHandler(jdbcHandlerMap);
    }
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}
private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
  TypeHandler<?> soleHandler = null;
  for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
    if (soleHandler == null) {
      soleHandler = handler;
    } else if (!handler.getClass().equals(soleHandler.getClass())) {
      // More than one type handlers registered.
      return null;
    }
  }
  return soleHandler;
}

獲取typeHandler有可能經歷三次:

  1. 根據指定的JdbcTypejdbcHandlerMap中獲取
  2. 若獲取失敗,用JdbcType=nulljdbcHandlerMap中獲取
  3. 還獲取失敗,判斷jdbcHandlerMap只否僅存在一個typeHandler,如果是,那麼就是它了!(pickSoleHandler)

總結

本文在開頭給兩個註解@MappedTypes@MappedJdbcTypes下了定義,強調了兩者之間的不同之處。接着,結合案例對@MappedTypes的使用進行分析,論述瞭如何解決類膨脹的問題。之後,結合源碼分析經過@MappedTypes@MappedJdbcTypes註解的TypeHandler註冊過程,得出了註解方式與註冊主流程不強相關,不存在強依賴關係的結論。最後,通過源碼對官網的闡述(Since Mybatis 3.4.0 however ...)進行迴應,可知,Mybatis確實在向着越來越好用的方向演進…


導讀:Mybatis優雅存取json字段的解決方案 - TypeHandler (一)

發佈了15 篇原創文章 · 獲贊 2 · 訪問量 3227
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章