Lombok 与 Mybatis 踩坑日记

Lombok 是一种 Java 实用工具,可帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO),它通过注解来实现这一目的。但是当 Lombok 和 Mybatis 相遇时,会产生一些意想不到的结果。

问题描述

今天发版,监控线上日志。发现存在如下报错:

日志截图

关键堆栈信息

1
2
3
4
5
6
7
8
9
10
11
12
13
org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'rule_desc_code' from result set.  Cause: java.sql.SQLException: Bad format for Timestamp '301' in column 13.
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:83)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createUsingConstructor(DefaultResultSetHandler.java:671)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:654)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:618)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:591)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:397)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)

产生疑问:rule_desc_code 明明是 String 类型,怎么会映射成 Timestamp 类型呢?它跟第 13 列又有什么关系?

原因定位

根据完整堆栈中的信息(未贴出),定位到报错的是 PayRouteRuleRecordMapper.xml 中的 selectByGatewayPayNos 方法。

仔细检查了一遍,发现 SQL 没有问题。

继续查看堆栈信息,找到了如下报错类。

于是按照方法一步一步点进去,终于找到了这里。发现 Mybatis 会将构造器中的入参与查询结果中的参数一一对应起来。

再来查看实体类、建表语句、构造方法。仔细查看建表语句和构造方法中的第 13 个字段。

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@Builder
public class PayRouteRuleRecord implements Serializable {
private Long id;
private Long memberId;
private String gatewayPayNo;
private String merchantOrderNo;
private String siteId;
private String siteUid;
private String payMethod;
private String ruleType;
private String ruleDesc;
private String ruleDescCode;
private String ruleTrack;
private Date createTime;
private Date lastUpdateTime;
private Boolean isDel;
private Integer retryFlag;

}

建表语句

构造方法

一切疑惑都解开了。

  • 因为 PayRouteRuleRecord 类中加了 @Builder 注解,Lombok 会自动生成全参构造方法,构造器中的参数顺序与实体类中定义的顺序保持一致。
  • ruleDescCode 字段是后加的,实体类中字段的顺序与 sql 语句中的顺序不一致,从而导致 Mybatis 将 ruleDescCode 字段映射到了 lastUpdateTime 字段上。 也就有了开头的报错 Error attempting to get column 'rule_desc_code' from result set. Cause: java.sql.SQLException: Bad format for Timestamp '301' in column 13.

修复方案

方案一

既然知道了问题是因为字段顺序不一致导致的,那么调整一下实体类的字段顺序,使其与 sql 语句中的顺序一致即可。

  • 优点
    • 修复快。
  • 缺点
    • 没有从根本上修复该问题。
    • 如果其他人调整了实体类的字段顺序,此问题还会出现。

还有更优的修复方案吗?

方案二

阅读 DefaultResultSetHandler.createResultObject() 方法时发现,如果实体类中存在无参构造方法,就会通过 objectFactory.create(resultType) 方法直接生成结果对象(利用反射实现),不用再一一比对实体类和返回结果中的字段。

所以只需要给实体类加上 @AllArgsConstructor 和 @NoArgsConstructor 注解,让实体类拥有无参构造方法即可。

修复前

修复后

小结

  • @Data 注解会给实体类的所有属性生成 get/set 方法,以及实现 hashCode()、toString()、equals() 方法,并且生成一个无参数的构造方法。
  • @Builder 注解除了会生成相应的 Build 模式的代码外,还会生成一个全参数的构造方法,参数的顺序就是属性定义的顺序
  • 但如果 @Data 注解和 @Builder 注解一块使用的话就只会生成全参构造方法,不会有无参构造方法。
  • 鉴于许多开源框架都会用到实体类的无参构造方法,因此建议 @Data、@Builder、@AllArgsConstructor、@NoArgsConstructor 四个注解同时使用,避免踩坑。

引用