问题现象
最近遇到一个问题,当 mybatis 的 mapper.xml 文件中使用来映射的对象使用了 lombok 中的 @Builder 注解修饰后,发生了字段值映射错位的问题,而当去掉 @Builder 注解之后字段映射值又正常了。
问题复现
为了复现这个问题,搞了一个 demo 代码。首先定义了一个 user 表,假设它里面只有 id, name, update_time 这三个字段,其中 name 是 varchar 类型, update_time 是 timestamp 类型。表的定义如下:
sql
CREATE TABLE user (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
name VARCHAR(30) NULL DEFAULT NULL ,
update_time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
然后定义了一个 User 类,它里面定义了 updateTime, name 字段,且 updateTime 字段的顺序在 name 注解前面,同时使用了 lombok 中的 @Builder 注解修饰。代码如下:
java
@Builder
@Data
public class User {
private Date updateTime;
private String name;
}
然后 mapper.xml 文件中定义了一个查询所有用户的语句,在这个语句中 select 的时候,name 字段的顺序在 update_time 前面,这点很重要,使用的 MyBatis 版本是 3.5.10。Xml 文件定义如下:
java
<mapper namespace="com.mapper.UserMapper">
<resultMap id="userMap" type="com.entity.User">
<result property="name" column="name"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="listAll" resultMap="userMap">
select name, update_time
from user
</select>
</mapper>
在数据库初始化的时候往 user 表里面插入了一条数据。SQL 如下:
sql
INSERT INTO user (id, name) VALUES (1, 'Jone');
执行查询后接口直接报错了,从报错看是尝试把 Jone 这个字符串转为 Timestamp 类型从而报错了,实际报错是在 DateTyperHanlder 的 getNullableResult() 方法里面触发的。如下图所示: 

而当把 @Builder 注解去掉之后使用 @Datae 注解修饰,再次执行查询,接口返回的结果就正常了。如下图所示:
java
@Data
public class User {
private Date updateTime;
private String name;
}

为啥使用了 @Builder 就会报错,去掉之后就正常了,接下来将从源码角度来分析一下。
问题原因
在 MyBatis 中是通过 ResultSetHandler 来对查询结果进行处理的。首先它会通过反射的方式创建一个对象的实例,在使用 @Builder 注解和不使用注解的区别对于 User 类来说是 @Builder 注解会生成一个全参的构造函数,而不使用的情况下,则会有一个默认的无参构造函数。如下图所示: 
在 MyBatis 中的 DefaultResultSetHandler 的 createResultObject() 方法实现了通过反射来创建对象,当是全参构造函数时走的的是 createByConstructorSignature() 方法这个分支。代码如下:
java
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
String columnPrefix)
throws SQLException {
final Class<?> resultType = resultMap.getType();
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
if (hasTypeHandlerForResultObject(rsw, resultType)) {
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
} else if (!constructorMappings.isEmpty()) {
return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) { // 如果有无参构造函数直接走这里就创建对象了
return objectFactory.create(resultType);
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
在 createByConstructorSignature() 方法实现了通过构造函数的方式来反射创建对象。代码如下:
java
private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class<?> resultType,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
"No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
}
在创建之前会通过 findConstructorForAutomapping() 方法来查找一个合适的构造函数,当类的构造函数只有一个时,会直接使用这个这个构造函数。代码如下:
java
private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
Constructor<?>[] constructors = resultType.getDeclaredConstructors();
// 只有一个构造函数时,直接使用这个构造函数
if (constructors.length == 1) {
return Optional.of(constructors[0]);
}
for (final Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(AutomapConstructor.class)) {
return Optional.of(constructor);
}
}
if (configuration.isArgNameBasedConstructorAutoMapping()) {
// Finding-best-match type implementation is possible,
// but using @AutomapConstructor seems sufficient.
throw new ExecutorException(MessageFormat.format(
"'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
resultType.getName()));
} else {
return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
}
}
然后在 applyConstructorAutomapping() 方法中会调用 applyColumnOrderBasedConstructorAutomapping() 方法。从该方法的命名也可以看出,它是实现了根据下标,而不是参数名称来实现构造参数的映射。代码如下:
java
private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
boolean foundValues = false;
if (configuration.isArgNameBasedConstructorAutoMapping()) {
// 这个是根据构造参数的名称映射
foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
constructor, foundValues);
} else {
// 这个是根据构造参数的顺序映射
foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
foundValues);
}
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}
在 applyColumnOrderBasedConstructorAutomapping() 方法中会从下标取构造函数中的参数,同时根据下标取查询结果里面的列名,先调用 rsw.getTypeHandler() 找到对应的 TypeHandler ,然后调用 TypeHandler 的 getResult() 方法尝试将列对应的值转为对应的构造函数的参数类型。代码如下:
java
private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
Class<?> parameterType = constructor.getParameterTypes()[i];
String columnName = rsw.getColumnNames().get(i);
// 先找到对应的TypeHandler
TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
// 调用TypeHandler将列对应的值转为对应的构造参数类型
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
constructorArgTypes.add(parameterType);
constructorArgs.add(value);
foundValues = value != null || foundValues;
}
return foundValues;
}
在本案例中构造函数的第一个参数类型是 Date 类型,第一个列名是 name。首先根据列名是找不到对应的 TypeHandler 的,然后调用 TypeHandlerRegistry 的 getTypeHandler() 方法获取对应的 TypeHandler。
java
public TypeHandler<?> getTypeHandler(Class<?> propertyType, String columnName) {
TypeHandler<?> handler = null;
// 根据列名找不到对应的TypeHandler
Map<Class<?>, TypeHandler<?>> columnHandlers = typeHandlerMap.get(columnName);
if (columnHandlers == null) {
columnHandlers = new HashMap<>();
typeHandlerMap.put(columnName, columnHandlers);
} else {
handler = columnHandlers.get(propertyType);
}
if (handler == null) {
JdbcType jdbcType = getJdbcType(columnName);
handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
// 省略代码
}
return handler;
}
在这个方法里面会先尝试根据 jdbcType 作为 key 获取对应的 TypeHandler,但是获取不到,然后尝试根据 null 作为 key 获取对应的 TypeHandler,这个实际上是获取到的是 DateTypeHandler 这个对象的实例。代码如下:
java
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
// 先根据jdbcType作为key获取对应的TypeHandler
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
// 根据null作为key获取,这里会获取到DateTypeHandler
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
而 DateTypeHandler 则是在 TypeHandlerRegistry 的构造函数里面注册的,注册的时候恰好是 null 作为 key 的。代码如下:
java
public TypeHandlerRegistry(Configuration configuration) {
this.unknownTypeHandler = new UnknownTypeHandler(configuration);
// 省略其它代码
register(Date.class, new DateTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
// 这里注册了key为null的DateTypeHandler
register(javaType, null, typeHandler);
}
}
在本案例中 DateTypeHandler 的 getNullableResult() 方法中传入的列名是 name,然后调用 ResultSet 的 getTimestamp() 方法获取 Timestamp 类型的实例,因为实际上这个列的值是 Jone ,当然是转不成 Timestamp 类型的,因此报错了。代码如下:
java
public Date getNullableResult(ResultSet rs, String columnName)
throws SQLException {
Timestamp sqlTimestamp = rs.getTimestamp(columnName);
if (sqlTimestamp != null) {
return new Date(sqlTimestamp.getTime());
}
return null;
}
当去掉 @Builder 注解时,使用的构造函数就是无参构造函数,直接通过无参构造函数就创建对象了,自然就不需要按照构造参数下标将列转为对应的构造参数类型了。代码如下:
java
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
throws SQLException {
final Class<?> resultType = resultMap.getType();
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
if (hasTypeHandlerForResultObject(rsw, resultType)) {
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
} else if (!constructorMappings.isEmpty()) {
return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) { // 如果有无参构造函数直接走这里就创建对象了
return objectFactory.create(resultType);
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}