每篇一句
在窮的時候餓的時候,千萬對一個人說物質不重要。基礎不牢,地動山搖
為了追查此問題根源,本人通過複現現象、控制變量、調試源碼等方式,苦心全身心投入連續查找近4個小時,終于找出端倪。現通過本文分享給大家,希望對各位有所幫助。
問題背景
為了簡化持久層的開發,減少或杜絕重複SQL的書寫,提高開發效率和減少維護成本,本人基于MyBatis寫了一個操作DB的中間件。為了規範操作,中間件提供了一個帶泛型化參數的抽象類供以繼承(BaseDBEntity),利用泛型的模版特性,來實作統一控制(包括統一查詢、統一分頁處理等等)。BaseDBEntity部分源碼:
public abstract class BaseDBEntity<T extends BaseDBEntity<T, PK>, PK extends Number> {
private PK id;
... //省略get、set方法
}
貼上我們問題子產品Entity的繼承情況:
public class SupplementDomain extends BaseDBEntity<SupplementDomain, Integer> {
private Long teacherId;
private String operateNo;
... //省略其餘屬性和get/set方法
}
但是查詢後,情況如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yYyIDZiJWYyEmN1QjY1QDN1MDZkNjNwcTNkZWZzATZx8CX5AzLcdDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL4M3Lc9CX6MHc0RHaiojIsJye.png)
我從結果集裡就能看出來,id現在是一個BigInteger類型的值。這就詭異了,根據上面的的代碼繼承結構,SupplementDomain這個類明明應該是Integer類型才對(備注:此問題我咋一看其實并不陌生,因為SpringMVC也有類似的Bug存在,這“得益于”Java的泛型的根本問題,有點無解。參考博文:【小家java】為什麼說Java中的泛型是“假”的?(通過反射繞過Java泛型))。
因為存在這樣的直接原因,導緻我們哪怕隻執行簡單的
Integer id = bean.getId(); //類型轉換異常
都會報錯。隻要不操作它,才相安無事。是以具有極大的安全隐患,雖然我已告知使用的同僚處理的辦法,但是并沒有知道其根本原因,心裡着實不踏實。是以才有了本文,無奈隻能撸源碼,看看MyBatis到底是怎麼樣把這給封裝錯了的。
源碼分析
偌大的MyBatis源碼,從哪下手呢?我首先擺出了它的四大核心元件:
ParameterHandler 、ResultSetHandler 、StatementHandler 、Executor
很顯然,根據我對MyBatis的了解,
ResultSetHandler
首當其沖。跟着源碼一層一層探讨一下MyBatis把資料庫記錄集映射到POJO對象的一個簡要的過程。
根據之前有大概看過幾大核心對象的源碼,是以我知道
ResultSetHandler
隻有一個一個實作類:
DefaultResultSetHandler
,是以沒什麼好說的,進去看吧,封裝結果集的入口方法:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException { }
Tip:從解析結果集裡面可以看出,MyBatis是先new出來了一個List multipleResults,是遵循盡量少的null元素的設計的。是以Dao層查出來的List,以後都不用判斷Null,清晰了代碼結構
内部核心,其實是循環調用了handleResultSet方法,是以主要跟蹤這個方法:
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
handleRowValues方法把處理後的結果清單添加到List内(multipleResults内),是以其實我們可以得出一個初步結論:不管方法handleRowValues裡面調用的層次多深,最終把結果集ResultSet經過處理,得到了需要的那些POJO對象并存儲到一個List裡面。是以我們重點看看handleRowValues方法,先看斷點後的幾張資料截圖:
從圖中可以看到,此處Mybatis已經把一些元資訊(包括Java類字段、資料庫字段、映射關系、處理器等)都已經準備好了,接下類就是用這個方法去封裝一行資料到一個java的POJO。
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
方法中分兩種情況分别調用了兩個方法,前一種是resultMap中有嵌套(MyBatis支援嵌套子查詢Select),後一種沒有嵌套,這裡重點看看後一種方法:
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
簡單一浏覽就能看到,這裡最重要的方法,就是getRowValue:
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(resultObject);
boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
resultObject = foundValues ? resultObject : null;
return resultObject;
}
return resultObject;
}
這個方法需要好好讀一下,它做的事是把一行資料封裝成一個Java對象,是以第一步可以看到它調用了
createResultObject
方法建立了一個對象,方法内部較為複雜,但我們簡單了解為它就是通過反射給我newInstance了一個空對象:
備注lazyLoader表示的是否要延遲加載,這是MyBatis的一個特性:支援懶加載。我們預設都是實時加載的
這裡面非常重要的一個方法
applyPropertyMappings
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
// issue #377, call setter on nulls
if (value != DEFERED
&& property != null
&& (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()))) {
metaObject.setValue(property, value);
}
if (property != null && (value != null || value == DEFERED)) {
foundValues = true;
}
}
}
return foundValues;
}
其實在這裡可以窺視到從資料表的列如何映射到對象的屬性的一點端倪了:
- 先把resultMap中取得的列名轉換為大寫字母,再截取它的字首(去除特殊字元),把這個字首和要映射到的對象的屬性進行比對,符合的就映射過去,即對POJO對象注入對應屬性值。是以這裡是不受到字母大小寫的影響的 從此處需要注意了,for循環裡已經按照資料庫表列為次元,一個一個的處理了。這裡面有一行代碼必要詳細看一下:
【小家MyBatis】MyBatis封裝結果集時,Integer類型的id字段被指派成了Long類型---讀源碼找原因
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
還記得我們最前面說的Id被指派為BigInteger了嗎?是以我猜測就是這裡的value值自己本身就是BigInteger的類型了,看看
getPropertyMappingValue
咋寫的:
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
if (propertyMapping.getNestedQueryId() != null) {
return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
} else if (propertyMapping.getResultSet() != null) {
addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
return DEFERED;
} else {
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);
}
}
到了這一步其實我就比較更為熟悉了。
調試看到了這個,思路就越來越清晰了。很顯然,就是處理轉換的類型轉換器竟然是
UnKonownTypeHandler
,是以給我們轉換成了什麼鬼呢?為了一探究竟我們跟蹤到它的
getNullableResult
方法:
@Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
TypeHandler<?> handler = resolveTypeHandler(rs, columnName);
return handler.getResult(rs, columnName);
}
private TypeHandler<?> resolveTypeHandler(ResultSet rs, String column) {
try {
Map<String,Integer> columnIndexLookup;
columnIndexLookup = new HashMap<String,Integer>();
ResultSetMetaData rsmd = rs.getMetaData();
int count = rsmd.getColumnCount();
for (int i=1; i <= count; i++) {
String name = rsmd.getColumnName(i);
columnIndexLookup.put(name,i);
}
Integer columnIndex = columnIndexLookup.get(column);
TypeHandler<?> handler = null;
if (columnIndex != null) {
handler = resolveTypeHandler(rsmd, columnIndex);
}
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
return handler;
} catch (SQLException e) {
throw new TypeException("Error determining JDBC type for column " + column + ". Cause: " + e, e);
}
}
看到問題的又一根源了,MyBatis完全根據資料庫中id字段的類型來推斷Java類型,而這種推斷又依賴于這部分代碼
這是非常不好的一種處理方式,因為Map裡面的值竟然采用自然排序,然後通過index去識别,顯然就非常有問題。
是以我們看到的現象是,有的有問題,有的沒有問題。有的問題的方式并且都不盡相同,有的成了Long,有的成了BigInteger
我個人認為這是MyBatis設計另一個很失敗的地方,可以定義為一個bug級别的存在。關鍵它還是“軟病”,讓我着實花了好久找到此處。後續希望自己可以提個issue被采納
那我們看到了此處被選中的為BigInteger的轉換器,是以自然而然得到的值類型如下:
是以,最直接的問題,我們隻剩下一個了,為何BigInteger類型的值,可以被set到Integer類型的Id上面。那就繼續跟蹤這句代碼:
metaObject.setValue(property, value);
setValue
方法如下:
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
// don't instantiate child path if value is null
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
objectWrapper.set(prop, value);
}
}
執行了objectWrapper.set(prop, value);這一句,調用的objectWrapper的方法。這裡其實會引申出好幾個問題,objectWrapper怎麼來的?暫時不讨論這快,接着往下走吧。調試發現objectWrapper其實是個
BeanWrapper
,是以我們點進去看看它的set方法:
@Override
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
setCollectionValue(prop, collection, value);
} else {
setBeanProperty(prop, object, value);
}
}
//主要執行的方法
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = {value};
try {
method.invoke(object, params);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
}
}
這裡面重點就來了,關鍵就在于metaClass.getSetInvoker(prop.getName()); 中的這個metaClass屬性,它其實就是我上面說到的元資訊的概念(該理念在流行架構的設計中經常用的),它包含有set方法的資訊:
看看我們關心的id屬性:
oh my god。中繼資料裡面儲存的根本就不是我們以為的setId(Integer id)這種,而是保留有父類自己的東西。是以我們自然就好了解了,為什麼set進去一個
BigInteger
值竟然也不抱錯的原因了(它也繼承了Number類)。
到此,我們就算把出現這種現象的原因完全給弄明白了。
but,but,but。這其實并沒有徹底的讓我“心服口服”,至少有兩大問題一直困擾着我,沒有找到根本原因。
疑問提出
(此處暫時隻提出兩個問題做出解答,更加詳細的,可以關注後續我的撸管MyBatis源碼專題)
1、為何getValue比對類型轉換器的時候,找到的是UnknownTypeHandler?
(本問題此處大概講一下,更加詳細的,MyBatis的類型轉換器模式,完全需要拉一個專題出來講解)
MyBatis内部注冊和維護了幾乎所有的類型轉換器,是以我們平時使用的時候根本就不用管,它自動就能跟我們比對上,轉換成我們需要的結果。在初始化的時候,有個轉換器注冊類:
TypeHandlerRegistry
:(列出部分)
我們會發現3.4版本的MyBatis對 JSR 310标準的日期時間也提供了支援
順帶我們可以看一下,架構更新給我們帶來的優雅體驗:
我們發現3.4.6版本處理起來,就優雅很多,大贊。
UnknownTypeHandler
對應的類型:
register(Object.class, UNKNOWN_TYPE_HANDLER);
register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
我們會發現,它對應的都是Object類型。
MyBatis在進行初始化的時候,會把所有的xml檔案裡的ResultMap進行注冊,并且提供全局通路。而當注冊到此處的繼承情況的時候,在擷取xml繼承的id類型的時候,因為是繼承的,是以拿不到實際類型,進而注冊不到對應的處理器,最終隻能交給
UnknownTypeHandler
處理
下面一個簡單的例子,大家可以感受一下MyBatis為啥注冊時候找不到了:
public static void main(String[] args) throws NoSuchFieldException {
//Field id = Son.class.getField("id");
//System.out.println(id); //若id是從父類繼承來的,傳入的泛型 java.lang.Number com.sayabc.boot2demo1.api.TestController$BaseEntity.id
//System.out.println(id); //id是本類自己的屬性 public java.lang.Integer com.sayabc.boot2demo1.api.TestController$Son.id
//另外一種方式(屬性都是private的都ok)
Field id = Son.class.getSuperclass().getDeclaredField("id");
System.out.println(id); //我們會發現擷取的SuperClass的 類型直接是java.lang.Number 根本沒得商量
}
class Son extends BaseEntity<Integer> {
//public Integer id;
private String name;
}
//此處為了友善反射 屬性用public的
class BaseEntity<PK extends Number> {
public PK id;
}
我們能夠得出結論。當屬性是從父類繼承過來的,反射去擷取這個字段的類型,它的類型是父類類型。(本例如果沒有繼承自Number,那傳回的就是Object類型)
2、為何剛看到的中繼資料metaClass對象儲存的是父類的setId方法呢?作何考慮?這個值又是什麼時候被指派放進去的呢?
這幾個問題其實相對來說比較簡單些,如果熟悉流行開源架構的這方面的設計思想,發現都是通的,大家都這麼“玩”。是以這個問題我這裡就不做解答了,留給讀者自己思考一番吧
MyBatis結果集如果是Map遇上泛型的話,也是可能遇上同樣問題的。
說到最後
架構能極大提高我們的開發效率,甚至我們可以基于開源本身定制出更符合我們業務的架構。
一件事本身的複雜度不會減少,它隻是從一個地方轉移到了另外一個地方而已,總的複雜度是恒定不變的,這是一個定理。