这个故事是这样开始的,某部门的小伙伴,突然有一天找我,火急火燎的说线上业务出问题了。之前线上运行的代码没有任何问题,突然某功能的一条 SQL 执行异常。之前是好好的啊,怎么突然就不行了。查询了好多资料,也检查了代码,也不知道错在了哪里。
通过分析,发现了一个很奇怪的现象,SQL 语句写的是没有任何问题的,通过开启 SQL 语句 Debug 打印,其实也显示查询到了记录,但是返回给业务层的时候,始终没有数据,返回 java.lang.NullPointerException 异常。
根据经验判断,应该是 mybatis 代码的问题,经过询问得知,小伙伴使用了一个开源框架,他升级了这个框架,附带地 mybatis 的版本也跟着升级了。
有了这个答案,似乎问题就简单很多了,我们只要找到问题是如何引起的就解决了。我们首先想到应该是mybatis在做 resultMap 映射的时候出了异常。我们进入 ResultSetHandler 的实现类 DefaultResultSetHandler 中寻找答案。
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 处理单行RowValue
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
// 处理自动映射
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
for (UnMappedColumnAutoMapping mapping : autoMapping) {
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
// 创建自动映射,主要是在这个方法中
private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
final String mapKey = resultMap.getId() + ":" + columnPrefix;
// 解析出未映射的列,返回的结果
List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
if (autoMapping == null) {
autoMapping = new ArrayList<>();
// 从resultSet中获取未映射的列名
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
// 循环分析处理
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
if (columnPrefix != null && !columnPrefix.isEmpty()) {
// When columnPrefix is specified,
// ignore columns without the prefix.
if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
propertyName = columnName.substring(columnPrefix.length());
} else {
continue;
}
}
// 看下列在最终的对象中是否存在
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
// 如果存在,就进行自动映射处理
if (property != null && metaObject.hasSetter(property)) {
/* 如果resultMap中包含这个列,退出本轮循环。
问题就出在这里,如果查询返回的列在resultMap中有显示定义,那么就会结束本次处理;
这时候存在这样一个问题,如果一张表中有5个字段,resultMap中有三个字段,
且对应于表中三个字段,那么mybatis会认为这三个字段已经映射了,
导致autoMapping为空,这时候就会出现业务返回空指针,这个应该是一个bug
*/
if (resultMap.getMappedProperties().contains(property)) {
continue;
}
final Class<?> propertyType = metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
} else {
configuration.getAutoMappingUnknownColumnBehavior()
.doAction(mappedStatement, columnName, property, propertyType);
}
} else {
configuration.getAutoMappingUnknownColumnBehavior()
.doAction(mappedStatement, columnName, (property != null) ? property : propertyName, null);
}
}
autoMappingsCache.put(mapKey, autoMapping);
}
return autoMapping;
}
通过上面的代码分析,我们找到了问题的根源,mybatis 会把表中返回的字段能够对应 resultMap 属性字段过滤掉。这就导致当返回的字段和 resultMap 全部对应时,返回的 autoMapping 映射 List 为空,自然导致映射失败。
既然我们已经知道了问题存在这一行代码,那怎么修改呢?
if (resultMap.getMappedProperties().contains(property)) {
continue;
}
这里有几种方法:一、是使用 resultType,因为我们问题是单表的查询直接映射,理论上讲应该使用 resultType,这时候不去解析 resultMap 映射,这样就避免了过滤,但是线上有可能还有其他功能存在这样的使用情况,逐一去修改是不可能的。
二、回退框架 ,回退到设计之前的版本,但是小伙伴反馈,以基于新版本开发,线上已经有新版本特性的代码,回退是不可能回退的,这辈子都不可能回退的,其他的也干不了,只能依靠第三种方法。
三、比对了升级的代码,发现 mybatis 从 3.4.3 引入了这行代码,日志显示是为了解决复杂 map 的嵌套 bug,避免无意义的解析。在我们的实际使用场景中,没有使用这样的情况,于是我们决定走第三种方案,修改源码。
但是问题来了,修改了源码将来再次升级 mybatis 怎么办,会不会忘记这个 bugfix?最终我们决定这样做,我们将这个文件单独修改,同时生成一个 bugfix 的 jar 包,基于 JVM 的 classLoader 双亲委托特性,想办法让 JVM 优先加载我们 jar 包里的这个类,这样既避免了修改 mybatis 源码,也可以不用修改线上业务代码,还能解决我们的问题,将来也可以平滑升,一举多得。最终经过验证,成功解决。
总结
- 对于不熟悉的框架,升级需谨慎;
- 开源的框架问题,不要害怕,勇敢的看他的源码;
- 遇到解决不了的问题,不妨换个思路,从其他方向寻找突破。这才是这次问题最宝贵的经验,问题本身不重要,重要的是解决的思路。