異常產(chǎn)生場景及異常信息
上周,由于在Mybatis的Mapper接口方法中使用實現(xiàn)了Map.Entry接口的泛型類,同時此方法對應(yīng)的sql語句也使用了foreach標簽,導(dǎo)致出現(xiàn)了異常。如下為異常信息:
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還不是非常的了解,導(dǎo)致用了很長的時間去debug找具體的異常原因。
接下來介紹我將重現(xiàn)異常并分析導(dǎo)致異常的原因。
異常重現(xiàn)
為了重現(xiàn)上面的異常,寫了demo。相關(guān)代碼如下:
數(shù)據(jù)庫表結(jié)構(gòu):
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 參數(shù)類:
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事務(wù)管理--> <transactionManager type= "JDBC" /> <!-- 數(shù)據(jù)庫連接池,整合后一般使用第三方的連接池--> <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文件的主要內(nèi)容:
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> |
以上為重現(xiàn)此異常的主要代碼,完整代碼可在Github查看:https://github.com/misterzhou/java-demo/tree/master/test-spring/spring-server
重現(xiàn)異常的測試代碼 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(); } } } |
賣個關(guān)子,大家先不要往下看,想想導(dǎo)致異常的原因(熟練使用foreach標簽的同學(xué)應(yīng)該能看出端倪)。
查找異常過程及異常分析
在項目中,由于DAO方法的參數(shù)類和返回結(jié)果類經(jīng)常會包含一個鍵和鍵對應(yīng)的值,為了避免重復(fù)定義類,我定義了一個實現(xiàn)了Map.Entry接口的KeyValue泛型類,具體請查看上節(jié)。
GoodsRoomnight30daysMapper.insertBatch()
方法就使用了此泛型類。在運行之后就拋出了本文開始提到的異常信息。
看到異常信息后,就把重點放到了是不是Mybatis對泛型的支持不夠好上。問了下同事(@勝男),同事在自己的機器上試了下,發(fā)現(xiàn)沒有異常。這就奇怪了。仔細看了下代碼,發(fā)現(xiàn)不同之處就是我的KeyValue泛型類實現(xiàn)了Map.Entry接口。 此時還不知道m(xù)ybatis官網(wǎng)對于foreach標簽的說明:
可以將任何可迭代對象(如列表、集合等)和任何的字典或者數(shù)組對象傳遞給foreach作為集合參數(shù)。當(dāng)使用可迭代對象或者數(shù)組時,index是當(dāng)前迭代的次數(shù),item的值是本次迭代獲取的元素。當(dāng)使用字典(或者Map.Entry對象的集合)時,index是鍵,item是值。
接下來就是通過debug看看,異常產(chǎn)生的具體原因。于是就先用實現(xiàn)了Map.Entry接口的KeyValue類的代碼進行debug,通過異常日志可以看到異常是在 DefaultSqlSession.java:200 行拋出,那么將斷點打到 DefaultSqlSession.java:197行,然后一步步向下執(zhí)行,當(dāng)執(zhí)行到 ForEachSqlNode.java:73行時,終于煥然大悟。先看下debug調(diào)用鏈圖:
再看看具體的代碼 ForEachSqlNode.java:73(此類就是foreach標簽對象的Node類):
此時具體的異常原因就很明顯了,此處的 o 對象的所屬的類就是KeyValue類,由于KeyValue類實現(xiàn)了Map.Entry接口,所以 o instance Map.Entry 為true,mybatis就把key值賦給了foreach的index屬性,而把value值賦給了item屬性,此處也就是把值為5的Integer對象賦給了item屬性。所以 GoodsRoomnight30daysMapper.xml 中id為 insertBatch 的select標簽的item屬性對應(yīng)的對象也就沒有 item.key 和 item.value 屬性,這就是最終導(dǎo)致異常的原因。
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標簽使用不當(dāng)導(dǎo)致異常的原因淺析,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對服務(wù)器之家網(wǎng)站的支持!
原文鏈接:https://my.oschina.net/u/553773/blog/813260