异常产生场景及异常信息
上周,由于在Mybatis的Mapper接口方法中使用实现了Map.Entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,导致出现了异常。如下为异常信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'key' in 'class java.lang.Integer'
### The error may involve org.guojing.test.spring.server.GoodsRoomnight30daysMapper.insertBatch-Inline
### The error occurred while setting parameters
### SQL: REPLACE INTO goods_roomnight_30days (goods_id, checkin_room_night_30days) VALUES (?, ?) , (?, ?), (?, ?), (?, ?)
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'key' in 'class java.lang.Integer'
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java: 30 )
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java: 200 )
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java: 185 )
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java: 57 )
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java: 53 )
at com.sun.proxy.$Proxy4.insertBatch(Unknown Source)
at org.guojing.test.spring.server.GoodsRoomnight30daysTest.test(GoodsRoomnight30daysTest.java: 47 )
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java: 57 )
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java: 43 )
at java.lang.reflect.Method.invoke(Method.java: 606 )
at org.junit.runners.model.FrameworkMethod$ 1 .runReflectiveCall(FrameworkMethod.java: 47 )
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java: 12 )
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java: 44 )
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java: 17 )
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java: 26 )
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java: 27 )
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java: 271 )
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java: 70 )
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java: 50 )
at org.junit.runners.ParentRunner$ 3 .run(ParentRunner.java: 238 )
at org.junit.runners.ParentRunner$ 1 .schedule(ParentRunner.java: 63 )
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java: 236 )
at org.junit.runners.ParentRunner.access$ 000 (ParentRunner.java: 53 )
at org.junit.runners.ParentRunner$ 2 .evaluate(ParentRunner.java: 229 )
at org.junit.runners.ParentRunner.run(ParentRunner.java: 309 )
at org.junit.runner.JUnitCore.run(JUnitCore.java: 160 )
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java: 68 )
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java: 51 )
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java: 237 )
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java: 70 )
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'key' in 'class java.lang.Integer'
at org.apache.ibatis.reflection.Reflector.getGetInvoker(Reflector.java: 409 )
at org.apache.ibatis.reflection.MetaClass.getGetInvoker(MetaClass.java: 164 )
at org.apache.ibatis.reflection.wrapper.BeanWrapper.getBeanProperty(BeanWrapper.java: 162 )
at org.apache.ibatis.reflection.wrapper.BeanWrapper.get(BeanWrapper.java: 49 )
at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java: 122 )
at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java: 119 )
at org.apache.ibatis.mapping.BoundSql.getAdditionalParameter(BoundSql.java: 75 )
at org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(DefaultParameterHandler.java: 72 )
at org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize(PreparedStatementHandler.java: 93 )
at org.apache.ibatis.executor.statement.RoutingStatementHandler.parameterize(RoutingStatementHandler.java: 64 )
at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java: 86 )
at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java: 49 )
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java: 117 )
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java: 198 )
... 29 more
|
由于本人对Mybatis还不是非常的了解,导致用了很长的时间去debug找具体的异常原因。
接下来介绍我将重现异常并分析导致异常的原因。
异常重现
为了重现上面的异常,写了demo。相关代码如下:
数据库表结构:
1
2
3
4
5
|
CREATE TABLE `goods_roomnight_30days` (
`goods_id` bigint( 20 ) NOT NULL,
`checkin_room_night_30days` int ( 11 ) NOT NULL DEFAULT '0' COMMENT '近30天消费间夜' ,
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT= 'goods的近30天消费间夜'
|
KeyValue.java 参数类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class KeyValue<K, V> implements Map.Entry<K, V> {
private K key;
private V value;
public KeyValue() {
}
public KeyValue(K key, V value) {
this .key = key;
this .value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
V oldValue = this .value;
this .value = value;
return oldValue;
}
public JSONObject toJSONObject() {
return ReportJSONObject.newObject().append(String.valueOf(key), value);
}
@Override
public String toString() {
return toJSONObject().toJSONString();
}
}
|
DAO类GoodsRoomnight30daysMapper.java
1
2
3
4
5
|
public interface GoodsRoomnight30daysMapper {
int deleteByExample(GoodsRoomnight30daysExample example);
List<GoodsRoomnight30days> selectByExample(GoodsRoomnight30daysExample example);
<K, V> int insertBatch(List<KeyValue<K, V>> records);
}
|
mybatis-config.xml文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<?xml version= "1.0" encoding= "UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
<settings>
<setting name= "cacheEnabled" value= "false" />
</settings>
<!-- 和spring整合后 environments配置将废除,交给spring管理-->
<environments default = "development" >
<environment id= "development" >
<!-- 使用jdbc事务管理-->
<transactionManager type= "JDBC" />
<!-- 数据库连接池,整合后一般使用第三方的连接池-->
<dataSource type= "POOLED" >
<property name= "driver" value= "com.mysql.jdbc.Driver" />
<property name= "url" value= "jdbc:mysql://localhost:3306/hotel_report?characterEncoding=utf-8" />
<property name= "username" value= "test_user" />
<property name= "password" value= "user123" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource= "mybatis/GoodsRoomnight30daysMapper.xml" />
</mappers>
</configuration>
|
GoodsRoomnight30daysMapper.xml文件的主要内容:
1
2
3
4
5
6
7
8
9
10
11
|
<insert id= "insertBatch" parameterType= "list" >
REPLACE INTO goods_roomnight_30days (goods_id, checkin_room_night_30days)
VALUES
<foreach collection= "list" index= "index" item= "item" separator= "," >
(#{item.key}, #{item.value})
<!--<choose>-->
<!--<when test= "item.value == null" >(#{item.key}, 0 )</when>-->
<!--<when test= "item.value != null" >(#{item.key}, #{item.value})</when>-->
<!--</choose>-->
</foreach>
</insert>
|
以上为重现此异常的主要代码,完整代码可在Github查看:https://github.com/misterzhou/java-demo/tree/master/test-spring/spring-server
重现异常的测试代码 GoodsRoomnight30daysTest.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package org.guojing.test.spring.server;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.guojing.spring.commons.KeyValue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Created at: 2016-12-24
*
* @author guojing
*/
public class GoodsRoomnight30daysTest {
SqlSessionFactory sqlSessionFactory;
SqlSession sqlSession;
GoodsRoomnight30daysMapper goodsRoomnight30daysMapper;
@Before
public void init() throws IOException {
String resource = "mybatis/mybatis-config.xml" ;
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession( true );
goodsRoomnight30daysMapper = sqlSession.getMapper(GoodsRoomnight30daysMapper. class );
}
@Test
public void test() {
List<KeyValue<Long, Integer>> records = new ArrayList<>();
records.add( new KeyValue<Long, Integer>(1725L, 5 ));
records.add( new KeyValue<Long, Integer>(1728L, 3 ));
records.add( new KeyValue<Long, Integer>(1730L, null ));
records.add( new KeyValue<Long, Integer>(1758L, null ));
int deleted = goodsRoomnight30daysMapper.deleteByExample( new GoodsRoomnight30daysExample());
System.out.println( "----- deleted row size: " + deleted);
int row = goodsRoomnight30daysMapper.insertBatch(records);
System.out.println( "----- affected row: " + row);
List<GoodsRoomnight30days> result = goodsRoomnight30daysMapper.selectByExample( new GoodsRoomnight30daysExample());
for (GoodsRoomnight30days item : result) {
System.out.println(item.toString());
}
}
@After
public void after() {
if (sqlSession != null ) {
sqlSession.close();
}
}
}
|
卖个关子,大家先不要往下看,想想导致异常的原因(熟练使用foreach标签的同学应该能看出端倪)。
查找异常过程及异常分析
在项目中,由于DAO方法的参数类和返回结果类经常会包含一个键和键对应的值,为了避免重复定义类,我定义了一个实现了Map.Entry接口的KeyValue泛型类,具体请查看上节。
GoodsRoomnight30daysMapper.insertBatch()
方法就使用了此泛型类。在运行之后就抛出了本文开始提到的异常信息。
看到异常信息后,就把重点放到了是不是Mybatis对泛型的支持不够好上。问了下同事(@胜男),同事在自己的机器上试了下,发现没有异常。这就奇怪了。仔细看了下代码,发现不同之处就是我的KeyValue泛型类实现了Map.Entry接口。 此时还不知道mybatis官网对于foreach标签的说明:
可以将任何可迭代对象(如列表、集合等)和任何的字典或者数组对象传递给foreach作为集合参数。当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者Map.Entry对象的集合)时,index是键,item是值。
接下来就是通过debug看看,异常产生的具体原因。于是就先用实现了Map.Entry接口的KeyValue类的代码进行debug,通过异常日志可以看到异常是在 DefaultSqlSession.java:200 行抛出,那么将断点打到 DefaultSqlSession.java:197行,然后一步步向下执行,当执行到 ForEachSqlNode.java:73行时,终于焕然大悟。先看下debug调用链图:
再看看具体的代码 ForEachSqlNode.java:73(此类就是foreach标签对象的Node类):
此时具体的异常原因就很明显了,此处的 o 对象的所属的类就是KeyValue类,由于KeyValue类实现了Map.Entry接口,所以 o instance Map.Entry 为true,mybatis就把key值赋给了foreach的index属性,而把value值赋给了item属性,此处也就是把值为5的Integer对象赋给了item属性。所以 GoodsRoomnight30daysMapper.xml 中id为 insertBatch 的select标签的item属性对应的对象也就没有 item.key 和 item.value 属性,这就是最终导致异常的原因。
1
2
3
4
5
6
7
|
<insert id= "insertBatch" parameterType= "list" >
REPLACE INTO goods_roomnight_30days (goods_id, checkin_room_night_30days)
VALUES
<foreach collection= "list" index= "index" item= "item" separator= "," >
(#{item.key}, #{item.value})
</foreach>
</insert>
|
以上所述是小编给大家介绍的Mybatis foreach标签使用不当导致异常的原因浅析,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!
原文链接:https://my.oschina.net/u/553773/blog/813260