@Builder注解导致mybatis类型匹配错误 Cannot determine value type from string
@Builder注解
@Builder 注解是 Lombok 提供的一个非常强大的功能,它用于自动生成构建器模式的代码,使得对象的创建和初始化变得清晰、灵活且易于阅读。其核心原理可以概括为:在编译时,通过注解处理器(Annotation Processor)分析源代码的抽象语法树(AST),并自动生成新的Java代码。
比如,实体类加@Builder注解:
@Builder
public class CcsZprkCallback extends BaseEntity
{private static final long serialVersionUID = 1L;/** 主键,自增id */@TableId(value = "id", type = IdType.AUTO)private Long id;/** ccslogid */@Excel(name = "ccslogid")private Long ccsLogId;...
}
编译之后,通过Idea的反编译功能,查看CcsZprkCallback.class文件,可以发现一下几点变化:
- 增加了一个全参构造器
CcsZprkCallback(Long id, Long ccsLogId, String taskId, String orderId, String oppSys, String fromOrTo, String clientId, String reqUrl, Date reqTime, String method, BigDecimal quantity, String stockNo, Integer respCode, String respMsg, Date respTime, Long status) {this.id = id;this.ccsLogId = ccsLogId;this.taskId = taskId;this.orderId = orderId;this.oppSys = oppSys;this.fromOrTo = fromOrTo;this.clientId = clientId;this.reqUrl = reqUrl;this.reqTime = reqTime;this.method = method;this.quantity = quantity;this.stockNo = stockNo;this.respCode = respCode;this.respMsg = respMsg;this.respTime = respTime;this.status = status;} - 没有无参构造器,所以你不能通过new CcsZprkCallback()方式创建对象了,而且他的全参构造器也不是public的,不在同一个包下的话你也不能通过全参构造器创建实例对象。
- 提供了一个内部类CcsZprkCallbackBuilder,是构建器模式中的构建器,可以说是唯一可以提供示例创建的方式,我们只能通过这个构建器、给这个构建器传递参数,然后在通过这个构建器的build方法来实例化对象。
- 提供了一个静态的builder方法来获取上面提到的构建器:
public static CcsZprkCallback.CcsZprkCallbackBuilder builder() {return new CcsZprkCallback.CcsZprkCallbackBuilder();}
- 构建器的包含了原类(例子中的CcsZprkCallback)的所有属性,并且可以支持通过链式调用的方式给构建起的属性赋值。
- 构建器提供一个build方法,调用原类的全参构造器创建原类的实例对象
public CcsZprkCallback build() {return new CcsZprkCallback(this.id, this.ccsLogId, this.taskId, this.orderId, this.oppSys, this.fromOrTo, this.clientId, this.reqUrl, this.reqTime, this.method, this.quantity, this.stockNo, this.respCode, this.respMsg, this.respTime, this.status);}
- 如果你的类中没有equals和hashcode方法,会提供一个极其复杂的equals方法,判断所有属性都相等的情况下,两个实体相等,也会提供一个hashcode方法。
@Builder注解实现的功能其实就这么多,非常简单,个人感觉也没提供什么强大的功能,类似于语法糖,可以让喜欢的人写出来看起来比较高大上的链式调用代码。
但是,我们需要特别注意的是,由于@Builder注解去掉了缺省无参构造器,如果你在Mybatis的实体类加了@Builder注解,会导致mybatis在执行sql查询之后匹配字段的时候出现bug,而且非常诡异,不容易排查。
@Builder注解导致的mybatis字段匹配问题
mybatis实体类如果没有写无参构造器、使用了@Builder注解的话,会导致mybatis执行完sql查询后,匹配ResultSet中的字段到实体类属性的时候发生错配,导致诡异的bug:sql查询报错 Cannot determine value type from string …但是你不论是检查xml中ResultMap的字段 - 属性匹配关系,还是检查数据库中的字段类型、实体类中的属性类型,发现一切正常都没有问题、不应该报错。但是实际情况就是会报错。
导致错误的原因,其实@Builder就是罪魁祸首,我们可以从Mybatis源码大概找一下原因。Mybatis负责解析查询结果的类是DefaultResultSetHandler,在xml中配置的对应实体类没有无参构造器的情况下,会调用applyColumnOrderBasedConstructorAutomapping方法:
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 = (String)rsw.getColumnNames().get(i);TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);Object value = typeHandler.getResult(rsw.getResultSet(), columnName);constructorArgTypes.add(parameterType);constructorArgs.add(value);foundValues = value != null || foundValues;}return foundValues;}
最终会从resultSet中获取数据,然后调用实体类的全参构造器,也就是@Builder注解加入进来的构造器,然后,按照resultSet中的顺序、对应全参构造器中的参数顺序进行赋值。这样的话,就要求resultSet中从数据库获取到的字段顺序,和全参构造器中的参数顺序完全匹配。但是我们知道,这个匹配绝大部分情况下是不可能的,因为resultSet的字段顺序是sql语句中写的、全参构造器的顺序大概是实体类中属性定义的顺序,很难碰巧能对应。
如果不能对应的话,就会出现错配,最终在将resultSet中的数据赋值给实体类的实例时发生类型转换错误,你就会看到这个 Cannot determine value type from string错误:
public T getResult(ResultSet rs, String columnName) throws SQLException {try {return this.getNullableResult(rs, columnName);} catch (Exception var4) {throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + var4, var4);}}
怎么解决
一个办法是,尽可能不在mybatis实体类中用@Builder注解。
第二,如果你特别喜欢@Builder注解的话,必须保证实体类有无参构造器,要么手写一个,要么加@NoArgsConstructor注解。
