码上焚香

Yahocen

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 在映射查询结果到对象时,遵循以下规则:

  1. 优先使用无参构造方法​:如果存在无参构造方法,先创建对象,然后通过setter方法注入属性值

  2. 无无参构造时使用有参构造​:当没有无参构造方法时,尝试匹配有参构造方法

  3. 参数顺序严格匹配​:有参构造的参数顺序必须与查询结果的列顺序完全一致

  4. 参数数量必须匹配​:构造方法的参数数量必须与查询结果的列数相同

在我们的例子中:

  • 查询只选择了 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对象映射机制的一个重要细节:​当使用有参构造方法时,查询结果的列必须与构造参数严格匹配。理解这一机制,可以帮助我们避免类似的映射问题,写出更健壮的持久层代码。