欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Mybatis foreach标签使用不当导致异常的原因浅析

程序员文章站 2024-03-08 19:52:52
异常产生场景及异常信息 上周,由于在mybatis的mapper接口方法中使用实现了map.entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,...

异常产生场景及异常信息

上周,由于在mybatis的mapper接口方法中使用实现了map.entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,导致出现了异常。如下为异常信息:

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。相关代码如下:

数据库表结构:

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 参数类:

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

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文件的主要内容:

<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查看:

重现异常的测试代码 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对泛型的支持不够好上。问了下同事(@胜男),同事在自己的机器上试了下,发现没有异常。这就奇怪了。仔细看了下代码,发现不同之处就是我的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调用链图:

Mybatis foreach标签使用不当导致异常的原因浅析

再看看具体的代码 foreachsqlnode.java:73(此类就是foreach标签对象的node类):

Mybatis foreach标签使用不当导致异常的原因浅析

此时具体的异常原因就很明显了,此处的 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>

以上所述是小编给大家介绍的mybatis foreach标签使用不当导致异常的原因浅析,希望对大家有所帮助