MyBatis/MyBatis-Plus报错:java.lang.NullPointerException: target is null for method size的解决方法
作者:李少兄
一、问题背景
在使用 MyBatis 或 MyBatis-Plus 进行数据库操作时,开发者经常需要根据传入的参数动态构建 SQL 查询语句。例如,根据一组用户 ID(ids)进行批量查询。此时,通常会在 XML 映射文件中使用 <if> 标签配合 OGNL 表达式进行条件判断。
然而,如果对集合参数(如 List<Long> ids)未做充分的空值校验,就直接调用其方法(如 .size()),程序在运行时会抛出如下经典异常:
java.lang.NullPointerException: target is null for method size
at org.apache.ibatis.ognl.OgnlRuntime.callMethod(OgnlRuntime.java:1620)
at org.apache.ibatis.ognl.ASTMethod.getValueBody(ASTMethod.java:72)
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:171)
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:206)
at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:128)
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:171)
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:206)
at org.apache.ibatis.ognl.ASTGreater.getValueBody(ASTGreater.java:34)
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:171)
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:206)
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:408)
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:383)
at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:47)
at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32)
at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:34)
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.lambda$apply$0(MixedSqlNode.java:32)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32)
at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:39)
at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:320)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:69)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at jdk.proxy2/jdk.proxy2.$Proxy183.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154)
... 95 common frames omitted
✅ 关键报错信息:
java.lang.NullPointerException: target is null for method size at org.apache.ibatis.ognl.OgnlRuntime.callMethod
只要你在项目日志或控制台中看到上述堆栈信息,基本可以断定:你在 MyBatis 的 OGNL 表达式中直接调用了可能为 null 的集合对象(如 ids)的 .size() 方法。
二、问题原因分析
2.1 OGNL 表达式执行机制简述
MyBatis 使用 OGNL(Object-Graph Navigation Language) 来解析 XML 中 <if test="..."> 等标签内的表达式。当你在 XML 中写:
<if test="ids.size() > 0">
MyBatis 会在运行时尝试执行 ids.size()。但如果传入的 ids 参数为 null,JVM 就无法在 null 引用上调用任何实例方法,从而抛出 NullPointerException。
2.2 典型错误场景示例
假设你有一个根据用户 ID 列表查询用户的接口:
// Mapper 接口
List<User> selectByIds(@Param("ids") List<Long> ids);
对应的 XML 映射文件如下:
<!-- 错误写法 ❌ -->
<select id="selectByIds" resultType="User">
SELECT * FROM user
<where>
<if test="ids.size() > 0">
id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
</select>
当调用方传入 null 时:
userMapper.selectByIds(null); // ids = null
MyBatis 在解析 <if test="ids.size() > 0"> 时,会尝试调用 null.size(),立即触发 NPE。
注意:即使你认为“调用方不会传 null”,但在实际系统中,前端参数缺失、DTO 转换失败、单元测试遗漏等情况都可能导致 ids 为 null。因此,防御性编程是必须的。
三、正确解决方案
方案一:在 OGNL 表达式中显式判空(最常用且推荐)
修改 XML 中的 <if> 条件,先判断 ids 不为 null,再判断其大小:
<!-- 正确写法 ✅ -->
<if test="ids != null and ids.size() > 0">
id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
或者使用更语义化的 isEmpty() 方法(MyBatis 支持):
<if test="ids != null and !ids.isEmpty()">
...
</if>
为什么安全?
OGNL 表达式支持逻辑短路(short-circuit evaluation):若 ids != null 为 false,则整个表达式直接为 false,不会继续执行 ids.size(),从而避免 NPE。
方案二:Java 层保证集合非 null(防御性初始化)
在业务层或 Controller 层,确保传给 Mapper 的集合不为 null:
public List<User> getUsers(List<Long> ids) {
if (ids == null) {
ids = Collections.emptyList(); // 或 new ArrayList<>()
}
return userMapper.selectByIds(ids);
}
这样即使 XML 中写了 ids.size() > 0,也不会崩溃(因为 Collections.emptyList().size() 返回 0)。
但仍建议配合方案一使用,以增强代码鲁棒性和可读性。
方案三:使用 MyBatis-Plus 的条件构造器(彻底规避 XML 风险)
如果你使用的是 MyBatis-Plus,强烈建议优先使用其内置的 条件构造器(Wrapper),它天然支持条件判断,且类型安全、可读性强。
MyBatis-Plus 的 QueryWrapper / LambdaQueryWrapper 方法通常提供一个 boolean 类型的第一个参数,用于控制该条件是否生效。例如:
public List<User> selectUsersByIds(List<Long> ids) {
return lambdaQuery()
.in(ids != null && !ids.isEmpty(), User::getId, ids)
.list();
}
关键说明(来自 MyBatis-Plus 官方行为):
in(boolean condition, SFunction<T, ?> column, Collection<?> values)- 当
condition为true时,才将IN条件加入 SQL;否则忽略。 - 如果省略
condition参数(即只传列和值),则默认condition = true,条件总是生效。
因此,正确的做法是在第一个参数中完成完整的判空逻辑:
// ✅ 安全写法
lambdaQuery()
.eq(name != null && !name.trim().isEmpty(), User::getName, name)
.ge(age != null && age >= 0, User::getAge, age)
.in(ids != null && !ids.isEmpty(), User::getId, ids)
.list();
这种方式:
- 无需编写 XML;
- 避免 OGNL 表达式陷阱;
- 编译期即可发现类型错误;
- 逻辑清晰,易于维护。
四、最佳实践总结
| 场景 | 推荐做法 |
|---|---|
| 手写 MyBatis XML | 始终使用 ids != null and ids.size() > 0 或 ids != null and !ids.isEmpty() |
| 集合参数来源不可控(如 HTTP 请求) | Java 层主动初始化:ids = Optional.ofNullable(ids).orElse(Collections.emptyList()) |
| 使用 MyBatis-Plus | 优先使用 LambdaQueryWrapper,并在条件方法的第一个参数中完成判空逻辑 |
| 团队编码规范 | 禁止在 test 表达式中直接调用 .size()、.isEmpty() 等方法而不判空 |
五、扩展知识:MyBatis 对集合的“真值”处理
MyBatis 内部对集合类型有特殊处理规则:
一个非 null 且非空的集合在布尔上下文中被视为 true;否则为 false。
因此,以下写法在某些情况下也能工作:
<if test="ids">
<!-- 相当于 ids != null && ids.size() > 0 -->
</if>
但请注意:
- 该行为依赖于 MyBatis 内部的
OgnlOps.booleanValue()实现; - 在复杂嵌套表达式或自定义类型中可能表现不一致;
- 可读性较差,新成员可能不理解其含义。
✅ 结论:虽然 test="ids" 是合法的,但为了代码清晰、可维护、可移植,强烈建议显式写出 ids != null and !ids.isEmpty()。
六、如何快速定位与修复
- 搜索关键字:在项目中全局搜索
.size()、.isEmpty()出现在 XML 的test=属性中; - 静态检查:可通过 SonarQube 或自定义 Checkstyle 规则禁止此类写法;
- 单元测试覆盖:编写测试用例,传入
null、空集合、正常集合三种情况,验证 SQL 生成正确性; - 日志监控:在生产环境中监控
NullPointerException,若堆栈包含OgnlRuntime.callMethod,优先排查动态 SQL。
七、附录:OGNL —— MyBatis 动态 SQL 的底层引擎
很多开发者在使用 MyBatis 时知道 <if test="xxx"> 可以写表达式,但并不清楚其背后的原理。
7.1 什么是 OGNL?
OGNL(Object-Graph Navigation Language)是一种开源的表达式语言,最初由 Drew Davidson 创建,现由 Apache Commons 维护(Apache Commons OGNL)。它允许你通过简洁的字符串表达式访问和操作 Java 对象图中的属性、方法、集合等。
MyBatis 从早期版本起就采用 OGNL 作为其动态 SQL 表达式的解析引擎(替代了早期的 JSTL 和自定义语法)。
7.2 OGNL 在 MyBatis 中的应用场景
在 MyBatis 的 XML 映射文件中,以下标签的 test 或 collection 等属性均使用 OGNL 表达式:
<if test="..."><when test="..."><choose>/<when>/<otherwise><foreach collection="..."><bind name="..." value="..."/>
例如:
<if test="user.age > 18 and user.active == true"> <foreach collection="user.roles" item="role"> <bind name="likePattern" value="'%' + keyword + '%'" />
这些表达式都会被 MyBatis 交给 OGNL 引擎在运行时求值。
7.3 OGNL 表达式语法要点
| 表达式 | 说明 |
|---|---|
param | 访问参数对象的属性(如 @Param("user") User user → user.name) |
param != null | 判空 |
list.size() | 调用集合的 size() 方法(⚠️ 若 list 为 null 会抛 NPE) |
list.isEmpty() | 调用 isEmpty()(同样需先判空) |
map['key'] 或 map.key | 访问 Map |
@java.lang.Math@max(a, b) | 调用静态方法(需开启 static method access) |
true and false | 逻辑运算(支持 and/or/not,也支持 &&/` |
a > b ? 'yes' : 'no' | 三元运算符 |
重要限制:出于安全考虑,MyBatis 默认禁用 OGNL 的静态方法调用和构造函数调用,防止表达式注入攻击。
7.4 OGNL 的求值上下文(Context)
MyBatis 在执行 OGNL 表达式时,会构建一个特殊的上下文对象(ContextMap),其中包含:
- 所有通过
@Param注解命名的参数; - 一些内置变量,如
_parameter(整个参数对象)、_databaseId等。
例如:
void update(@Param("id") Long id, @Param("name") String name);
在 XML 中可直接使用 id 和 name,因为它们已被注入到 OGNL 上下文中。
7.5 为什么ids.size()会报 NPE?
OGNL 在执行 ids.size() 时,会:
- 从上下文中获取
ids变量; - 若
ids == null,则target = null; - 尝试调用
target.size()→ 即null.size()→ JVM 抛出NullPointerException; - MyBatis 捕获该异常并包装为带有明确提示的错误信息:
target is null for method size。
这本质上是一个 Java 层面的方法调用失败,而非 OGNL 本身的 bug。
7.6 如何安全使用 OGNL?
- 永远先判空再调方法:
obj != null and obj.method() - 避免复杂逻辑:不要在 OGNL 中写业务逻辑,应移至 Java 层;
- 慎用静态方法:除非明确配置允许;
- 理解短路求值:
a != null and a.b()是安全的,因为a == null时不会执行a.b()。
7.7 OGNL 与 SpEL 的区别
有些开发者会混淆 OGNL 和 Spring 的 SpEL(Spring Expression Language)。两者都是表达式语言,但:
| 特性 | OGNL | SpEL |
|---|---|---|
| 所属生态 | Apache / MyBatis | Spring Framework |
| 主要用途 | MyBatis 动态 SQL | Spring 配置、注解、Thymeleaf 等 |
| 静态方法调用 | 默认禁用(MyBatis) | 支持(T(java.lang.Math).random()) |
| 安全性 | 较高(受限) | 需注意表达式注入 |
记住:MyBatis 不使用 SpEL,只使用 OGNL。
以上就是MyBatis/MyBatis-Plus报错:java.lang.NullPointerException: target is null for method size的解决方法的详细内容,更多关于MyBatis/MyBatis Plus报错target is null的资料请关注脚本之家其它相关文章!
