回顧
上篇,我們分析了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
,能夠處理多種類型,如VARCHAR
、CHAR
等,一旦限定爲@MappedJdbcTypes(JdbcType.VARCHAR)
,就只能處理VARCHAR
。這樣會導致一個奇怪問題:在Mapper.xml
使用resultMap定義結果集映射,屬性未指定jdbcType
,Mybatis
就按照javaType=[TheJavaType], jdbcType=null
去TypeHandlerRegistry
中找,由於限定了自定義TypeHandler
的JdbcType
爲VARCHAR
,Map裏找不到,因此查找失敗。懶惰是程序員的天性,倘若是手寫的Mapper.xml
,很多開發人員並不會主動指定JdbcType
,程序就要報錯了。
爲了解決這個問題,@MappedJdbcTypes
有一個屬性includeNullJdbcType
用於指定是否將null
作爲JdbcType
進行註冊,默認值是false
。我們可以手動指定指爲true
,如此,Mybatis
按照javaType=[TheJavaType], jdbcType=null
去TypeHandlerRegistry
中就能找到我們自定義的TypeHandler
更進一步地,爲了讓MyBatis
用起來更方便一些,從MyBatis 3.4.0
開始,即便未指定includeNullJdbcType=true
,對於那些只存在一個TypeHandler
的JavaType
,TypeHandler
也會生效。再也不用擔心使用@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
有可能經歷三次:
- 根據指定的
JdbcType
從jdbcHandlerMap
中獲取 - 若獲取失敗,用
JdbcType=null
從jdbcHandlerMap
中獲取 - 還獲取失敗,判斷
jdbcHandlerMap
只否僅存在一個typeHandler
,如果是,那麼就是它了!(pickSoleHandler)
總結
本文在開頭給兩個註解@MappedTypes
、@MappedJdbcTypes
下了定義,強調了兩者之間的不同之處。接着,結合案例對@MappedTypes
的使用進行分析,論述瞭如何解決類膨脹的問題。之後,結合源碼分析經過@MappedTypes
、@MappedJdbcTypes
註解的TypeHandler
註冊過程,得出了註解方式與註冊主流程不強相關,不存在強依賴關係的結論。最後,通過源碼對官網的闡述(Since Mybatis 3.4.0 however ...
)進行迴應,可知,Mybatis
確實在向着越來越好用的方向演進…