前言
本文的創作來源於朋友在自學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中查詢的數據庫字段屬性的類型要和有參構造器的字段類型一一匹配;查詢字段的個數要和有參構造器個數一樣
最后當出現異常時,帶着問題去跟蹤源碼,有時候會比利用搜索引擎更容易得到答案