MyBatis映射值报错的罪魁祸首竟然是Lombok的@Builder详细说明并不只看表面做法,关键还要理解相关条件、限制和后续影响。
最近遇到一个问题,当 mybatis 的 mapper.xml 文件中使用来映射的对象使用了 lombok 中的 @Builder 注解修饰后,发生了字段值映射错位的问题,而当去掉 @Builder 注解之后字段映射值又正常了。
为了复现这个问题,搞了一个 demo 代码。首先定义了一个 user 表,假设它里面只有 id, name, update_time 这三个字段,其中 name 是 varchar 类型, update_time 是 timestamp 类型。表的定义如下:
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 注解修饰。代码如下:
@Builder @Data public class User { private Date updateTime; private String name; }然后 mapper.xml 文件中定义了一个查询所有用户的语句,在这个语句中 select 的时候,name 字段的顺序在 update_time 前面,这点很重要,使用的 MyBatis 版本是 3.5.10。Xml 文件定义如下:
<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 如下:
INSERT INTO user (id, name) VALUES (1, 'Jone');
执行查询后接口直接报错了,从报错看是尝试把 Jone 这个字符串转为 Timestamp 类型从而报错了,实际报错是在 DateTyperHanlder 的 getNullableResult() 方法里面触发的。如下图所示:


而当把 @Builder 注解去掉之后使用 @Datae 注解修饰,再次执行查询,接口返回的结果就正常了。如下图所示:
@Data public class User { private Date updateTime; private String name; }
为啥使用了 @Builder 就会报错,去掉之后就正常了,接下来将从源码角度来分析一下。
在 MyBatis 中是通过 ResultSetHandler 来对查询结果进行处理的。首先它会通过反射的方式创建一个对象的实例,在使用 @Builder 注解和不使用注解的区别对于 User 类来说是 @Builder 注解会生成一个全参的构造函数,而不使用的情况下,则会有一个默认的无参构造函数。如下图所示:

在 MyBatis 中的 DefaultResultSetHandler 的 createResultObject() 方法实现了通过反射来创建对象,当是全参构造函数时走的的是 createByConstructorSignature() 方法这个分支。代码如下:
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() 方法实现了通过构造函数的方式来反射创建对象。代码如下:
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() 方法来查找一个合适的构造函数,当类的构造函数只有一个时,会直接使用这个这个构造函数。代码如下:
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() 方法。从该方法的命名也可以看出,它是实现了根据下标,而不是参数名称来实现构造参数的映射。代码如下:
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() 方法尝试将列对应的值转为对应的构造函数的参数类型。代码如下:
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。
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 这个对象的实例。代码如下:
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 的。代码如下:
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 类型的,因此报错了。代码如下:
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 注解时,使用的构造函数就是无参构造函数,直接通过无参构造函数就创建对象了,自然就不需要按照构造参数下标将列转为对应的构造参数类型了。代码如下:
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);}