问题现象
最近遇到一个问题,当 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);
}
总结
到此这篇关于mybatis映射值报错的罪魁祸首竟然是lombok的@builder的文章就介绍到这了,更多相关mybatis映射值报错内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论