MyBatisPlus/MyBatis/iBatis 构造方法映射陷阱:从数组越界异常到解决方案
编辑
7
2025-05-20
问题背景
最近在项目开发中遇到了一个有趣的 MyBatis(或 iBatis)映射问题:当查询结果映射到没有无参构造方法的实体类时,出现了数组越界异常。这个问题看似简单,却隐藏着 MyBatis 对象映射机制的一个重要细节。
### Error querying database. Cause: java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
### The error may exist in tech/timearrow/arch/rbac/mapper/SysUserRoleMapper.java (best guess)
### The error may involve tech.timearrow.arch.rbac.mapper.SysUserRoleMapper.selectList
### The error occurred while handling results
### SQL: SELECT user_id FROM sys_user_role WHERE deleted=0 AND (role_id IN (?))
### Cause: java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:156)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:425)
... 133 common frames omitted
Caused by: java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.get(ArrayList.java:435)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyColumnOrderBasedConstructorAutomapping(DefaultResultSetHandler.java:788)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyConstructorAutomapping(DefaultResultSetHandler.java:776)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:727)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:689)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:659)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:411)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:366)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:337)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:310)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:202)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:61)
at tech.timearrow.arch.mybatis.LowerCaseResultSetInterceptor.intercept(LowerCaseResultSetInterceptor.java:22)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at com.sun.proxy.$Proxy472.handleResultSets(Unknown Source)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:66)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:80)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy188.query(Unknown Source)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:65)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:336)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:158)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:110)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:81)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at com.sun.proxy.$Proxy187.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154)
... 140 common frames omitted
问题重现
考虑以下代码:
// 查询只选择userId字段
sysUserRoleService.list(new LambdaQueryWrapper<SysUserRole>()
.select(SysUserRole::getUserId)
.in(SysUserRole::getRoleId, roleIds));
// 实体类定义
public class SysUserRole {
public SysUserRole(Long userId, Long roleId) {
this.userId = userId;
this.roleId = roleId;
}
private Long userId;
private Long roleId;
// getter/setter省略...
}
执行这段代码时,会抛出数组越界异常。为什么?
问题根源分析
MyBatis/iBatis 在映射查询结果到对象时,遵循以下规则:
优先使用无参构造方法:如果存在无参构造方法,先创建对象,然后通过setter方法注入属性值
无无参构造时使用有参构造:当没有无参构造方法时,尝试匹配有参构造方法
参数顺序严格匹配:有参构造的参数顺序必须与查询结果的列顺序完全一致
参数数量必须匹配:构造方法的参数数量必须与查询结果的列数相同
在我们的例子中:
查询只选择了 userId 一列
但构造方法需要两个参数 (userId 和 roleId)
MyBatis 尝试获取第二列时发现不存在,导致数组越界
解决方案大全
1. 添加无参构造方法(推荐)
public class SysUserRole {
public SysUserRole() {} // 添加无参构造
public SysUserRole(Long userId, Long roleId) {
this.userId = userId;
this.roleId = roleId;
}
// 其他代码...
}
优点:简单直接,兼容性好
适用场景:大多数情况下的首选方案
2. 保持查询列与构造参数一致
// 查询选择构造方法需要的所有字段
sysUserRoleService.list(new LambdaQueryWrapper<SysUserRole>()
.select(SysUserRole::getUserId, SysUserRole::getRoleId)
.in(SysUserRole::getRoleId, roleIds));
优点:保持构造方法不变
缺点:需要确保查询列与构造参数严格对应
3. 使用ResultMap明确映射关系
<resultMap id="sysUserRoleMap" type="SysUserRole">
<constructor>
<arg column="user_id" javaType="long"/>
<arg column="role_id" javaType="long"/>
</constructor>
</resultMap>
优点:映射关系明确,不受列顺序影响
缺点:需要维护XML配置
4. 使用@AutomapConstructor注解(MyBatis 3.4.2+)
public class SysUserRole {
@AutomapConstructor
public SysUserRole(Long userId, Long roleId) {
this.userId = userId;
this.roleId = roleId;
}
// 其他代码...
}
优点:注解方式简洁
缺点:需要较新版本的MyBatis
总结
这个"小"bug教会了我们MyBatis对象映射机制的一个重要细节:当使用有参构造方法时,查询结果的列必须与构造参数严格匹配。理解这一机制,可以帮助我们避免类似的映射问题,写出更健壮的持久层代码。
- 0
- 0
-
赞助
支付宝
微信
-
分享