上周,因为Mybatis的Mapper接口方法参数使用实现了Map.Entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,致使出现了异常。以下为异常信息:html
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还不是很是的了解,本能的怀疑是Mybatis对泛型的支持不够好致使。java
接下来介绍我是如何重现异常并分析致使异常的缘由。mysql
为了重现上面的异常,写了demo。相关代码以下:git
数据库表结构:github
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 参数类:spring
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.javasql
public interface GoodsRoomnight30daysMapper { int deleteByExample(GoodsRoomnight30daysExample example); List<GoodsRoomnight30days> selectByExample(GoodsRoomnight30daysExample example); <K, V> int insertBatch(List<KeyValue<K, V>> records); }
mybatis-config.xml文件:数据库
<?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文件的主要内容:apache
<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:
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对泛型的支持不够好上,因而问了下同事(@胜南),同事写了个Demo在本身的机器上试了下,发现没有异常。这就奇怪了,仔细看了下代码,不一样之处就是个人KeyValue泛型类实现了Map.Entry接口。此时还不知道mybatis官网对于foreach标签的说明(连接地址:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html#foreach):
能够将任何可迭代对象(如列表、集合等)和任何的字典或者数组对象传递给foreach做为集合参数。当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者Map.Entry对象的集合)时,index是键,item是值。
接下来就是经过debug看看,异常产生的具体缘由:
此时具体的异常缘由就很明显了,此处 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 属性,这就是最终致使异常的缘由。
<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>