在穷的时候饿的时候,千万对一个人说物质不重要。基础不牢,地动山摇
本文已被https://yourbatman.cn收录;女娲Knife-Initializr工程可公开访问啦;程序员专用网盘https://wangpan.yourbatman.cn;公号后台回复“专栏列表”获取全部小而美的原创技术专栏

你好,我是方向盘(YourBatman、方哥)

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本

为了追查此问题根源,本人通过复现现象、控制变量、调试源码等方式,苦心全身心投入连续查找近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,所以没什么好说的,进去看吧,封装结果集的入口方法:

@Overridepublic 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 itcolumn = 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 optionalfinal String property = propertyMapping.getProperty();// issue #377, call setter on nullsif (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循环里已经按照数据库表列为维度,一个一个的处理了。这里面有一行代码必要详细看一下:
 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方法:

 @Overridepublic 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 nullreturn;} 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方法:

@Overridepublic 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遇上泛型的话,也是可能遇上同样问题的。

说到最后

框架能极大提高我们的开发效率,甚至我们可以基于开源本身定制出更符合我们业务的框架。

一件事本身的复杂度不会减少,它只是从一个地方转移到了另外一个地方而已,总的复杂度是恒定不变的,这是一个定理。

(比如这次的撸源码调试找问题就非常耗时,从开始到搞明白花了整4个小时左右,耗时的原因关键是MyBatis自己存在我上面指出的软病,加大了定位问题的难度)

System.out.println("写得可还行?收藏再看吧,点个赞吧,分享下吧");
echo("关注公号【Java方向盘】 https://yourbatman.cn  https://wangpan.yourbatman.cn");
console.log("私聊【方向盘】:fsx1056342982");

我是方向盘(YourBatman、方哥):一个前25年还不会写Hallo World、早已毕业的大龄程序员。网瘾失足、清考、延期毕业、房产中介、保险销售、送外卖…是我不可抹灭的黑标签