前言
本文的创作来源于朋友在自学 mybatis 遇到的问题,问题如文章标题所示 Cannot determine value type from string ‘xxx’。他在网上搜索出来的答案基本上都是加上一个无参构造器,就可以解决问题。他的疑问点在于他实体没有使用无参构造器,而使用了有参构造器,有的查询方法不会报错,有的查询方法却报错了。下面将演示他出现的这种场景的示例。
注: mybatis 的搭建过程忽略,仅演示案例。案例代码取自朋友
示例
1、entity
public class Student {
private int id;
private String name;
private String email;
private int age;
public Student(String aa,int bb){System.out.println("=============== 执行 student 的有参数构造方法 aa ="+aa+"bb ="+bb+"================");
}
public int getId() {return id;}
public void setId(int id) {this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public String getEmail() {return email;}
public void setEmail(String email) {this.email = email;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}
2、dao
public interface StudentDao {Student getStudentById(int id);
List<Student> getStudents(@Param("myname") String name, @Param("myage") int age);
List<Student> getStudentByObj(Student student);
}
3、mapper.xml
<mapper namespace="com.academy.dao.StudentDao">
<select id="getStudentById" resultType="com.academy.domain.Student">
select id, name, email, age from student where id = #{sid}
</select>
<select id="getStudents" resultType="com.academy.domain.Student">
select id, name, email, age from student where name = #{myname} or age = #{myage}
</select>
<select id="getStudentByObj" resultType="com.academy.domain.Student">
select id, name, email, age from student where name = #{name} or age = #{age}
</select>
</mapper>
4、单元测试
@Test
public void testgetStudentById(){SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
Student student = dao.getStudentById(1034);
sqlSession.close();
System.out.println(student);
}
@Test
public void testgetStudents(){SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
List<Student> students = dao.getStudents("张三", 22);
sqlSession.close();
students.forEach(student -> System.out.println(student));
}
5、运行单元测试
从截图看出,当实体没有使用无参构造器时,出现朋友所说的有一些方法成功,一些方法报错,报错信息为
Cannot determine value type from string 'xxx'
采用网上介绍的方法,给实体加上无参构造器,如下:
public class Student {
private int id;
private String name;
private String email;
private int age;
public Student(){}
public Student(String aa,int bb){System.out.println("=============== 执行 student 的有参数构造方法 aa ="+aa+"bb ="+bb+"================");
}
再次运行单元测试
加上无参构造器,确实不报错。那我们是否就可以因为这样,就得出 mybatis 执行必须得加上无参构造器的结论呢?
我们再把实体的无参构造器去掉,如下
public class Student {
private int id;
private String name;
private String email;
private int age;
public Student(String aa,int bb){System.out.println("=============== 执行 student 的有参数构造方法 aa ="+aa+"bb ="+bb+"================");
}
同时把 mapper.xml 修改为如下
<mapper namespace="com.academy.dao.StudentDao">
<select id="getStudentById" resultType="com.academy.domain.Student">
select id, name, email, age from student where id = #{sid}
</select>
<select id="getStudents" resultType="com.academy.domain.Student">
select name, age from student where name = #{myname} or age = #{myage}
</select>
<select id="getStudentByObj" resultType="com.academy.domain.Student">
select id, name, email, age from student where name = #{name} or age = #{age}
</select>
然后再次运行单元测试
从截图可以看出,mybatis 加了有参构造器并不影响执行。只是有参构造器要成功运行的条件是
-
mapper.xml 中查询的数据库字段属性的类型要和有参构造器的字段类型一一匹配
- 其次查询字段的个数要和有参构造器个数一样
比如该示例的有参构造器为 string int,则 xml 中 select 语句的字段类型也得是 varchar 和 int
解密 Cannot determine value type from string ‘xxx’ 异常
一开始我们看到这个异常,我们可能会先去检查实体字段和数据库字段是不是一样,首先这个思路是没问题,一旦发现不是这个问题,我们可以转换一下思路,先预设一下可能出现这种问题场景,比如有没有可能是 mybatis 在执行数据库字段到实体字段类型映射的过程中出现转换错误。其次解决异常的终极大招就是带着问题去跟踪源码。
我们跟踪源码可以发现 `
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
这个类有个方法 createResultObject
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, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of" + resultType);
}
这个方法是根据结果集返回值的类型创建出相应的 bean 字段对象
1、当实体使用无参构造器时
mybatis 会调用 createResultObject 方法中
objectFactory.create(resultType)
其核心代码片段如下
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
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);
}
}
使用无参构造器创建对象
2、当实体使用有参构造参数
mybatis 会调用 createResultObject 方法中
createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
其核心代码片段如下
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];
String columnName = 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 ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}
这个代码片段里面有个 TypeHandler,这个是 mybatis 的类型处理器,用来处理 JavaType 与 JdbcType 之间的转换。
由代码我们看出,当实体使用有参构造函数时,会遍历有参构造参数个数,根据有参构造参数下标查找相应的数据库字段名称,根据有参构造字段类型以及数据库字段名称找类型处理器。然后使用 TypeHandler 来处理 JavaType 与 JdbcType 之间的转换。当转换异常,就会报
Cannot determine value type from string 'xxx'
总结
解决 Cannot determine value type from string ‘xxx’ 的方法有 2 种
- 实体加无参构造参数
- mapper.xml 中查询的数据库字段属性的类型要和有参构造器的字段类型一一匹配;查询字段的个数要和有参构造器个数一样
最后当出现异常时,带着问题去跟踪源码,有时候会比利用搜索引擎更容易得到答案