MyBatis报错没有getter就用@Param

MyBatis单参数时会把对象直接解析属性,多参数或要在XML里用对象名包裹属性时必须用@Param注解命名。报错「There is no getter for property named 'xxx'」通常是因为XML里用了对象名但方法参数没加@Param。建议无论几个参数都加@Param,代码更清晰。

写 MyBatis 的时候撞上这么个报错:

org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'sysUser' in 'class com.mebugs.demo.sys.entity.SysUser'
​

第一眼看到我是懵的:说 SysUser 里没有 sysUser 的 getter?SysUser 类里压根就没有 sysUser 这个属性啊。

折腾了一会儿才搞明白是怎么回事,顺便把 MyBatis 参数绑定的规则理清了。这篇就把这个坑和背后的门道说清楚。

报错长什么样

先看出事的代码。

Mapper 接口,一个按条件统计数量的方法,入参是整个 SysUser 对象:

/**
 * 按条件查询数量
 * @param sysUser 查询条件对象
 */
int count(SysUser sysUser);
​

对应的 Mapper.xml,条件里用 sysUser.xxx 的方式取属性:

<sql id="queryWork">
    <if test="sysUser != null and sysUser.userName != null and sysUser.userName != ''">
        and s.user_name LIKE CONCAT(#{sysUser.userName},'%')
    </if>
    <if test="sysUser != null and sysUser.userStatus != null">
        and s.user_status = #{sysUser.userStatus}
    </if>
</sql>
<select id="count" resultType="java.lang.Integer">
    SELECT count(user_id) FROM sys_user s WHERE 1=1
    <include refid="queryWork"/>
</select>
​

一跑,控制台就甩出上面那个 There is no getter for property named 'sysUser'。

类用了 Lombok 的 @Data,getter 肯定是全的。问题不在 getter,在于 MyBatis 到哪里去找 sysUser 这个属性。

它为什么找错了地方

关键在这里:当 Mapper 接口方法只有一个对象参数时,MyBatis 默认会把这个对象本身当成参数上下文,直接解析它的属性。

也就是说,XML 里能直接写 #{userName}、#{userStatus},MyBatis 会去 sysUser 对象上取同名属性。

但我在 XML 里写的是 sysUser.userName。

MyBatis 一看,就以为"你要从参数对象里取一个叫 sysUser 的属性,再取它的 userName"。

于是它跑到 SysUser 对象上找 getSysUser(),当然找不到,报错。

说白了,是我多包了一层对象名。参数对象是 SysUser,XML 里却又写了 sysUser.,等于让它在 SysUser 里再掏一个 sysUser 出来。

先验证一下猜想

为了确认是这个原因,我把 XML 里的 sysUser. 前缀去掉,直接写属性名:

<sql id="queryWork">
    <if test="userName != null and userName != ''">
        and s.user_name LIKE CONCAT(#{userName},'%')
    </if>
    <if test="userStatus != null">
        and s.user_status = #{userStatus}
    </if>
</sql>
​

再跑,正常了,日志里能看到参数正确绑进去:

==>  Preparing: SELECT count(user_id) FROM sys_user s WHERE 1=1 and s.user_name LIKE CONCAT(?,'%')
==> Parameters: mebugs(String)
<==      Total: 1
​

猜想坐实:单个对象参数时,直接用属性名取值,别再套对象名。

顺带提一句,单个基础类型参数也类似,可以直接用参数名取:

// 单个 int 参数,XML 里直接 #{userId} 就行
@Select("SELECT s.* FROM sys_user s WHERE s.user_id = #{userId}")
SysUser view(int userId);
​

多个参数怎么办

单参数好说,可一旦方法有多个参数,比如同时传分页对象和查询条件对象,就没法靠"默认解析"了,MyBatis 不知道该把哪个当上下文。

这时候得给参数起名字,用 @Param 注解。

@Param 干的事很简单:给每个参数命个名,XML 里就用这个名字当前缀去取属性。

/**
 * 分页查询用户列表
 * @param page 分页参数
 * @param sysUser 查询条件
 */
List<SysUser> page(@Param("page") Page page, @Param("sysUser") SysUser sysUser);
​

有了 @Param("sysUser"),XML 里的 sysUser.userName 这次就对了,因为现在确实存在一个叫 sysUser 的命名参数:

<sql id="queryWork">
    <if test="sysUser != null and sysUser.userName != null and sysUser.userName != ''">
        and s.user_name LIKE CONCAT(#{sysUser.userName},'%')
    </if>
</sql>
<select id="page" resultType="com.mebugs.demo.sys.entity.SysUser">
    SELECT s.* FROM sys_user s WHERE 1=1
    <include refid="queryWork"/>
    ORDER BY s.user_id DESC
    LIMIT #{page.start}, #{page.size}
</select>
​

同一个 queryWork 片段,之前报错,加了 @Param 后就能用——区别只在于参数到底有没有 sysUser 这个名字。

顺便说下 #{} 和 ${}

加了 @Param 还有个附带好处:命名之后 #{} 和 ${} 都能用;不命名的话某些场景只能用 #{}。

但这里得插一句安全提醒:日常一律用 #{},别用 ${}。

  • #{} 是预编译占位符,参数以 ? 的形式进去,防 SQL 注入。
  • ${} 是直接字符串拼接,用户输入什么就拼什么,等于给注入开了门。

只有排序字段名、表名这种没法用占位符的场景才考虑 ${},而且必须自己做白名单校验,绝不能直接接用户传的值。

小结

这个报错的根因就一句话:单个对象参数时 XML 里别再套对象名前缀,想套就得用 @Param 把名字补上。

我的习惯是不管一个参数还是多个,统一加 @Param。

好处有两个:一是彻底躲开这类"找不到 getter"的坑,参数到底叫什么一目了然;二是后面别人接手代码,看一眼就知道每个参数在 SQL 里对应什么,省得猜。

多写一个注解,换来可读性和稳定性,这买卖划算。

更多推荐

章节目录