由Lombok的@Builder注解引起的Mybatis创建返回对象异常
**结论:**使用了Bbuilder注解以后,会根据当前类生产一个全参构造函数(不包括父类的属性),Mybaitis对返回数据进行属性映射会出现很多奇怪的异常,所以最好不要使用@Builder注解,要用的话自己写一个,并且带一个默认无参构造函数。
1.@Builder的作用
使用了@Builder注解以后,创建对象的实例就不能直接new了,只能通过XXXX.builder().build()来进行创建,就算直接写一个无参构造也不行,编译不会通过。(离谱)
@Data
@ToString
@Builder
public class UserDO extends BaseDO {
public UserDO() {
}
private static final long serialVersionUID = 1643909999676303799L;
private String username;
private String password;
private String name;
private String tel;
private String isTel() {
return this.tel;
}
private String isT() {
return this.tel;
}
*E*rror:(29, 1) java: 无法将类 org.apache.ibatis.test.domain.UserDO中的构造器 UserDO应用到给定类型;* *需要: 没有参数* *找到: java.lang.String,java.lang.String,java.lang.String,java.lang.String* 原因: 实际参数列表和形式参数列表长度不同*
编译后的实体类(idea反编译)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class UserDO extends BaseDO {
private static final long serialVersionUID = 1643909999676303799L;
private String username;
private String password;
private String name;
private String tel;
private String isTel() {
return this.tel;
}
private String isT() {
return this.tel;
}
// 没有修饰符 默认protected
UserDO(String username, String password, String name, String tel) {
this.username = username;
this.password = password;
this.name = name;
this.tel = tel;
}
public static UserDO.UserDOBuilder builder() {
return new UserDO.UserDOBuilder();
}
// builder对象
public static class UserDOBuilder {
private String username;
private String password;
private String name;
private String tel;
UserDOBuilder() {
}
public UserDO.UserDOBuilder username(String username) {
this.username = username;
return this;
}
public UserDO.UserDOBuilder password(String password) {
this.password = password;
return this;
}
public UserDO.UserDOBuilder name(String name) {
this.name = name;
return this;
}
public UserDO.UserDOBuilder tel(String tel) {
this.tel = tel;
return this;
}
// build方法创建实体对象
public UserDO build() {
return new UserDO(this.username, this.password, this.name, this.tel);
}
public String toString() {
return "UserDO.UserDOBuilder(username=" + this.username + ", password=" + this.password + ", name=" + this.name + ", tel=" + this.tel + ")";
}
}
}
2.Mybaitis如何把返回数据封装成到对应的实体类
2.1.通过反射创建对应实体类
2.2.根据resultmap中对应属性信息,把属性set到实体类属性中去
创建返回实例
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createResultObject(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.util.List<java.lang.Class<?>>, java.util.List<java.lang.Object>, java.lang.String)
/**
*
* @param rsw ResultSe包装
* @param resultMap
* @param constructorArgTypes 构造函数类型集合
* @param constructorArgs 构造函数集合
* @param columnPrefix 数据库字段前缀
* @return
* @throws SQLException
*/
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
throws SQLException {
// 返回体class类型
final Class<?> resultType = resultMap.getType();
// 根据class类型创建的元数据类
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
// 构造函数映射集合
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
// 当前类类型存在于TypeHandler集合中
if (hasTypeHandlerForResultObject(rsw, resultType)) {
// 直接通过TypeHandler解析返回
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, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
mybatis中创建实例的类DefaultObjectFactory
其中存在两个create方法,通可以根据无参和有参构造函数创建对象
// DefaultObjectFactory实现的接口
public interface ObjectFactory {
/**
* Sets configuration properties.
* @param properties configuration properties
*/
default void setProperties(Properties properties) {
// NOP
}
/**
* Creates a new object with default constructor.
*
* @param <T>
* the generic type
* @param type
* Object type
* @return the t
*/
<T> T create(Class<T> type);
/**
* Creates a new object with the specified constructor and params.
*
* @param <T>
* the generic type
* @param type
* Object type
* @param constructorArgTypes
* Constructor argument types
* @param constructorArgs
* Constructor argument values
* @return the t
*/
<T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
/**
* Returns true if this object can have a set of other objects.
* It's main purpose is to support non-java.util.Collection objects like Scala collections.
*
* @param <T>
* the generic type
* @param type
* Object type
* @return whether it is a collection or not
* @since 3.1.0
*/
<T> boolean isCollection(Class<T> type);
}
创建对象的方法
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
// 根据构造方法的newInstance()创建
Constructor<T> constructor;
if (constructorArgTypes == null || constructorArgs == null) {
// 无参构造
constructor = type.getDeclaredConstructor();
try {
return constructor.newInstance();
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true); // 修改修饰符属性
return constructor.newInstance();
} else {
throw e;
}
}
}
// 传入的第一个构造函数
constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
try {
// 有参构造函数创建
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} else {
throw e;
}
}
} catch (Exception e) {
String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList)
.stream().map(Class::getSimpleName).collect(Collectors.joining(","));
String argValues = Optional.ofNullable(constructorArgs).orElseGet(Collections::emptyList)
.stream().map(String::valueOf).collect(Collectors.joining(","));
throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
}
}
3.产生异常的环节
3.1.通过有参构造函数创建的实例造成参数类型异常
查询返回字段的类型和构造方法入参的数据类型是按照顺序进行匹配的,这就会产生类型不一致问题
3.2.通过有参构造函数创建的实例造成属性数量小于返回数据属性长度
假如你的查询语句为select id from xxx where xxx
但是你的do对象的构造方法有两个入参,这时就会会抛出实例入参长度大于返回参数长度的问题。
具体如下:
select语句
<select id="getUser" resultType="org.apache.ibatis.test.domain.UserDO" >
// 查询参数为三个
select id,username,password
from m3_user
where username = #{username}
</select>
do对象
@Builder
public class UserDO extends BaseDO {
private static final long serialVersionUID = 1643909999676303799L;
private String username;
private String password;
private String name;
private String tel;
private String isTel() {
return this.tel;
}
private String isT() {
return this.tel;
}
}
全参构造方法入参为四个
// 使用带参构造方法创建对象
private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
boolean foundValues = false;
// 循环构造参数集合
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
Class<?> parameterType = constructor.getParameterTypes()[i];
//根据索引i从ResultSet中获取对应数组下表的数据字段,数组长度为select返回字段长度,假如构造方法参数长度大于返回字段数组长度,就会产生数组越界java.lang.IndexOutOfBoundsException
String columnName = rsw.getColumnNames().get(i);
// 假如select语句第一个返回的字段为string,但是构造函数入参第一个为其他类型,比如long,那么将发生参数转换异常java.lang.NumberFormatException:
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 ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}
4.如何避免这种异常发生
1.自己写一个builder类,并且默认无参构造方法为public
2.不要用lombook相关构造器的注解
3.写了默认有参数构造方法,对应xml中的select属性加上resultmap,并且写一个对应构造器的resultmap