最近接手一個老項目,進行json類型字段的對象映射,使用的是老版本的mybatis-plus(2.1.8),出現了一些問題
1、@TableFiled註解沒有typeHandler屬性,只能通過@TableField(el = "filed, typeHandler=xxx.TypeHandler")這種方式來配置
2、配置了@TableField(el = "filed, typeHandler=xxx.TypeHandler")之後,進行數據的新增操作沒有問題,會正常對象轉成json插入到數據庫,但是查詢的時候字段值為null,沒有查詢出來,於是開始了排查
因為我有多個對象,json字段都是List,但泛型類型不一樣,可以看下mybatis的BaseTypeHandler<T>類,繼承自TypeReference<T>,這個TypeReference<T>類卻不支持泛型的解析,可以看下源碼
TypeHandler<T>
TypeReference<T>
BaseTypeHandler<T>
在繼承 MyBatis 的 TypeReference<T> 時發現 rawType 是 List 而不是 List<String>,這是因為 MyBatis 故意這樣設計的。
看這段關鍵代碼:
Type rawType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
// TODO remove this when Reflector is fixed to return Types
if (rawType instanceof ParameterizedType) {
rawType = ((ParameterizedType) rawType).getRawType();
}
MyBatis 主動將 ParameterizedType(如 List<String>)轉換為其原始類型(List),為什麼要這樣做? 註釋中提到 "TODO remove this when Reflector is fixed to return Types",説明這是臨時措施
所以我們如果有多個List泛型的字段,需要一些特殊處理,mybatis底層會把類型作為key,handler作為value存到一個TYPE_HANDLER_MAP的map裏,所以如果直接用這個是沒法實現泛型的處理的
那有什麼辦法呢,於是我繼續源碼的追蹤,可以看到typeHandler註冊邏輯
public <T> void register(TypeHandler<T> typeHandler) {
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);
}
}
MappedTypes註解是定義在TypeHandler實現類上的,可以聲明該TypeHandler作用在哪種類型上,然後進入到
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}
這裏可以看到 (TypeHandler<T>) c.newInstance(javaTypeClass);説明繼承TypeHandler的類構造函數可以有個傳參,這個傳參就是一個擴展點,可以根據你傳入的class類型做不同的處理,如果定義了MappedTypes註解,那就會把MappedTypes註解的值作為key來進行存儲,但是我們不能都設置List類型,因為前面説了我們是有泛型的,底層是個map,存多個List的key會覆蓋掉handler,那我們有沒有辦法來實現不同類型來表達不同的List泛型呢,是可以的,通過自定義類來繼承實現,比如
public class AList extends ArrayList<A> {}
public class BList extends ArrayList<B> {}
然後在我們實體類需要用到List的地方使用我們自定義的類,即
@Data
@TableName("x")
public class X {
private AList aList;
}
這樣就可以實現不同泛型的List處理
mybatis-plus自動生成的是走的自動映射
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
裏面會使用到typeHandlerRegistry.hasTypeHandler,判斷查詢字段是否有對應的typeHandler
因為mybatis-plus是隻做增強,這個老版本可能沒有做泛型的處理,新版本@TableField是支持配置typeHandler屬性,看了一下其他的文檔,好像mybatis-plus也是通過反射獲取到泛型類型後註冊到typeHandlerRegistry從而實現泛型的處理的
那為啥插入沒問題,查詢有問題呢,因為mybatis-plus實際上是會自己生成注入查詢方法和對應的sql到mybatis,從而實現不需要寫xml的效果,在插入的時候,會把註解的el同時給到mybatis(像什麼ew也是),mybatis就會根據sql生成下面這些對象,
MappedStatement,維護一條<select|update|delete|insert>節點的封裝
SqlSource,負責根據用户傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,並返回
BoundSql,表示動態生成的SQL語句以及相應的參數信息
,插入數據的時候就會執行List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
來設置每個參數,而這個parameterMappings是有typeHandler信息的
查詢的時候則沒有這個,而是會先判斷mybatis的AutoMappingBehavior屬性(控制是否自動映射),默認是PARTIAL,所以就會執行自動映射的邏輯
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
而自動映射就不會看字段配置的el了,而是直接獲取typeHandlerRegistry裏面的TypeHandler進行對應處理,所以沒效果
那如果我們把AutoMappingBehavior設置為NONE呢,試了一下,所有字段都沒有映射了,因為mybatis-plus生成的MappedStatement沒有設置resultMap,只設置了resultType,所以禁用自動映射的話字段都會為null,除非在@TableName註解裏指定了resultMap
public MappedStatement addSelectMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, Class<?> resultType,
TableInfo table) {
if (null != table) {
String resultMap = table.getResultMap();
if (null != resultMap) {
/* 返回 resultMap 映射結果集 */
return this.addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null, resultMap, null,
new NoKeyGenerator(), null, null);
}
}
/* 普通查詢 */
return this.addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null, null, resultType,
new NoKeyGenerator(), null, null);
}