为了追查此问题根源,本人通过复现现象、控制变量、调试源码等方式,苦心全身心投入连续查找近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方法 }
但是查询后,情况如下:
我从结果集里就能看出来,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的一个特性:支持懒加载。我们默认都是实时加载的