MyBatis if 标签的坑,居然被我踩到了。。。
事件的原因是这样的,需求是按条件查数据然后给前端展示就行了,写的时候想着挺简单的,不就是使用 MyBatis 动态 SQL 去查询数据吗?
现实还是很残酷的,等我写完上完 UAT 后,前端同学说根据state
查的数据与理想的数据不一致,这个state
当时设计时只有两个值:0
和1
。
/**
* 数据状态
*/
@Range(min = 0, max = 1, message = "状态只能为0(未处理),1(已处理)")
private Integer state;
理想情况下通过前端传递过来的值,然后进行sql查询就可以了:
<if test="req.state != null and req.state != ''">
AND md.state = #{req.state}
</if>
上面的sql首先判断state
不为空且不为空字符串时,然后添加比较state
字段。初步看下来if
判断没什么问题,但是我传递进去的req.state
是Integer
型的,仔细查看req.state != null
没毛病,然后发现req.state != ''
使用Integer
与空字符串做比较。
前端在查询的时如果没有传递req.state
那req.state != null
这里不会满足,但是前端传递了一个0
过来的时候req.state != ''
居然返回的是false
也就是说在MyBatis的if语法中0是等于空字符串的:
{
"state": 0
}
这样的比较没有报错,也是有点想不通了,没办法只能去看MyBatis源码找出这原因。
查看 MyBatis 源码
MyBatis 其他源码的查找过程就不详细说了,这里直接找到XMLScriptBuilder
类,找到if
语法的解析过程,然后一步步的探究0 == ''
的原因。 XMLScriptBuilder
会解析trim
、if
等 MyBatis 支持的语法,它的解析原理是通过NodeHandler
来分别解析不同的标签:
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
由于是不正解的语法是if
标签,查看IfHandler
就好了,其他现在略过就好。
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
MyBatis会将if
标签抽象成IfSqlNode
:
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
终于有一点眉头了, MyBatis 会将if
标签的test
属性使用ExpressionEvaluator
测试一下是否为true
或者为false
:
public class ExpressionEvaluator {
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
return (Iterable<?>) value;
}
if (value.getClass().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) {
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
}
最后得到结论:Mybatis 使用的 Ognl表达式
来获取 test 属性的值
最终论证
已经知道 MyBatis 内部是使用的 Ognl表达式
,是不是 Ognl表达式
的引起的呢? 实践一下就知道了,先引入依赖:
<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>2.7.3</version>
</dependency>
写程序测试:
public static void main(String[] args) {
Map<String, Object> objectMap = new HashMap<>();
objectMap.put("state", 0);
Object value = OgnlCache.getValue("state == ''", objectMap);
System.out.println(value);
}
上面程序输出的真的是true
。。。
总结
真是脑袋抽筋啊,Integer
还判断是否为空字符串。。。
记录此坑,希望对大家有所帮助。
推荐
欢迎关注公众号:架构文摘,获得独家整理120G的免费学习资源助力你的架构师学习之路!
公众号后台回复
arch028
获取资料:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
2019-12-09 5 分钟快速学习,缓存一致性优化方案!