背景
最近基于MyBatis(3.4.5)写了几个通用抽象类用以继承, 为了更通用些, 参数使用了泛型.
大致如下
抽象基类-BaseEntity
@Data
public abstract class BaseEntity<ID> implements Serializable {
private static final long serialVersionUID = 1L;
protected ID id;
}
抽象基类-CommonEntity
@Data
public abstract class CommonEntity<ID> extends BaseEntity<ID> {
private static final long serialVersionUID = 1L;
//省略其他通用属性
}
抽象基类-DataEntity
@Data
public abstract class DataEntity<ID> extends CommonEntity<ID> {
private static final long serialVersionUID = 1L;
//省略其他通用属性
}
然后我们有个表
CREATE TABLE `person` (
`id` int(11) unsigned NOT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
对应的实体Person, 继承了以上3个类
Person
@Data
public class Person extends DataEntity<Integer> {
//private Integer Id; 无需显式声明, 继承自基类
private String name;
private Integer age;
}
样使用, 按道理没什么问题, 咸鱼就写了几个查询, 结果一运行
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: java.lang.Long cannot be cast to java.lang.Integer; nested exception is com.fasterxml.jackson.databind.JsonMappingException: java.lang.Long cannot be cast to java.lang.Integer (through reference chain: java.util.ArrayList[0]->com.mrcoder.sbmannotations.domain.Person["id"])]
总儿言之,Long型无法转为Integer
当时 ,咸鱼就纳闷了, 不敢置信的从数据库定义到实体的属性统统检查了一遍, 从头到尾都没有发现有Long的定义, 那么这个Long从何而来? 又怎么会报这样的错??
带着疑惑, 咸鱼开始了Debug之旅.
究竟哪里开始报的错?
从错误的提示上, 我们根本没法找到哪一步开始错的, 没法子, 断点大法走起.
断点看出, sql查询出来id值的类型就是Long了.
这就诡异了,根据上面继承结构, Person这个类Id明明应该是Integer类型才对.
难道getPersonById方法有问题?
但我们的getPersonById方法实现很简单,就是直接mybatis执行了查询
无奈之下, 咸鱼尝试了各种方式(折腾), 发现直接显示在Person类声明
private Integer id;
就不会报错.
难道MyBatis结果集封装时对泛型类型支持有问题??
为了搞清这个问题, 不得不去扒一扒MyBatis的结果集封装实现了.
MyBatis 结果集封装
翻了翻MyBatis源码, 很快找到了
显然, ResultSetHandler就是专门处理结果集封装的接口类, IDEA跳转实现, 发现DefaultResultSetHandler是它的唯一实现类.
接下来, 我们继续断点大法来验证
直接在handleResultSets断点
发现跳转到DefaultResultSetHandler.handleResultSets
继续往下,发现到了DefaultResultSetHandler.getFirstResultSet
继续到了ResultSetWrapper.ResultSetWrapper(ResultSet rs, Configuration configuration) ,在这个构造方法里我们看到了希望
循环里就是在对每个字段进行类型、值的填充.
通过断点,我们验证了在这一步就赋值类型错误的事实.
那么问题来了, 为什么此处赋给id的class是Long?
仔细分析代码
final ResultSetMetaData metaData = rs.getMetaData();
这段获取数据库中的源数据,包含类型、值等信息, 接下来在一个for循环里把源数据进行了处理赋值给实体.
其中,以下这段完成了class的赋值
classNames.add(metaData.getColumnClassName(i));
我们继续断点进入metaData.getColumnClassName(i)方法
此时f.getMysqlType()拿到了数据库中id的声明类型为“INT UNSIGNED”,所以直接走到了switch的default分支
也就是f.getMysqlType().getClassName(),此时去MysqlType的枚举中获取className, 发现
到了这一步, 已经真相大白了!
根本原因
其实, 踩坑的的原因有两点.
- 我们使用了泛型抽象基类去指定了ID的类型
- 我们的数据库ID字段设置的类型为无符号的Int型
或许是Mybatis的“锅”, 又或者是表设计的问题, 总之, 避开以上任意一点, 就不会踩到此坑.