MyBatis 映射值报错的罪魁祸首竟然是 Lombok 的 @Builder?

问题现象

最近遇到一个问题,当 mybatis 的 mapper.xml 文件中使用来映射的对象使用了 lombok 中的 @Builder 注解修饰后,发生了字段值映射错位的问题,而当去掉 @Builder 注解之后字段映射值又正常了。

问题复现

为了复现这个问题,搞了一个 demo 代码。首先定义了一个 user 表,假设它里面只有 id, name, update_time 这三个字段,其中 namevarchar 类型, update_timetimestamp 类型。表的定义如下:

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 类型从而报错了,实际报错是在 DateTyperHanldergetNullableResult() 方法里面触发的。如下图所示:

而当把 @Builder 注解去掉之后使用 @Datae 注解修饰,再次执行查询,接口返回的结果就正常了。如下图所示:

java 复制代码
@Data  
public class User {  
    private Date updateTime;  
    private String name;  
}

为啥使用了 @Builder 就会报错,去掉之后就正常了,接下来将从源码角度来分析一下。

问题原因

在 MyBatis 中是通过 ResultSetHandler 来对查询结果进行处理的。首先它会通过反射的方式创建一个对象的实例,在使用 @Builder 注解和不使用注解的区别对于 User 类来说是 @Builder 注解会生成一个全参的构造函数,而不使用的情况下,则会有一个默认的无参构造函数。如下图所示:

在 MyBatis 中的 DefaultResultSetHandlercreateResultObject() 方法实现了通过反射来创建对象,当是全参构造函数时走的的是 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 的,然后调用 TypeHandlerRegistrygetTypeHandler() 方法获取对应的 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);
    }
}

在本案例中 DateTypeHandlergetNullableResult() 方法中传入的列名是 name,然后调用 ResultSetgetTimestamp() 方法获取 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);
}
相关推荐
一 乐2 小时前
景区管理|基于springboot + vue景区管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
superman超哥2 小时前
Rust 减少内存分配策略:性能优化的内存管理艺术
开发语言·后端·性能优化·rust·内存管理·内存分配策略
BingoGo2 小时前
CatchAdmin 2025 年终总结 模块化架构的进化之路
后端·开源·php
superman超哥2 小时前
Rust 并发性能调优:线程、异步与无锁的深度优化
开发语言·后端·rust·线程·异步·无锁·rust并发性能
superman超哥2 小时前
Rust Trait 对象与动态分发权衡:性能与灵活性的深度权衡
开发语言·后端·rust·rust trait·对象与动态发布·性能与灵活性
独断万古他化3 小时前
【Spring Web MVC 入门实战】实战三部曲由易到难:加法计算器 + 用户登录 + 留言板全流程实现
java·后端·spring·mvc
while(1){yan}3 小时前
Spring日志
java·后端·spring
予枫的编程笔记3 小时前
从入门到精通:RabbitMQ全面解析与实战指南
java·开发语言·后端·rabbitmq·ruby
superman超哥3 小时前
Rust 异步性能最佳实践:高并发场景的极致优化
开发语言·后端·rust·最佳实践·异步性能·高并发场景