前言
本文的创作来源于朋友在自学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中查询的数据库字段属性的类型要和有参构造器的字段类型一一匹配;查询字段的个数要和有参构造器个数一样
最后当出现异常时,带着问题去跟踪源码,有时候会比利用搜索引擎更容易得到答案