踩坑实录:Spring Boot 4.1.0 升级后 MongoDB Criteria 查询条件凭空消失?原理与修复方案全解析

踩坑实录:Spring Boot 4.1.0 升级后 MongoDB Criteria 查询条件凭空消失?原理与修复方案全解析 前言最近团队把项目的 Spring Boot 版本从 4.0.7 升到了 4.1.0本想着享受新版本带来的性能优化和新特性结果一跑测试就发现不对劲大量 MongoDB 的动态查询条件失效了最终生成的 Query 对象里条件为空查出来全表数据差点搞出线上事故。排查下来发现这不是业务代码写错了而是 Spring Data MongoDB 4.1 对Criteria的底层实现做了不兼容的破坏性变更。网上目前关于这个问题的资料极少今天就从现象到源码把这个坑给大家讲透。一、问题复现一模一样的代码升级后就废了先给大家看这段非常常见的动态查询写法相信很多人项目里都有类似的代码// 4.0.7 及之前版本正常运行4.1.0 条件直接失效 Criteria criteria new Criteria(); criteria.and(status).is(1); Query query Query.query(criteria); // 打印 query 会发现查询条件为空相当于查全表 System.out.println(query); // 4.0.7 输出Query: { status : 1}, Fields: {}, Sort: {} // 4.1.0 输出Query: {}, Fields: {}, Sort: {}就这么简单三行代码在 4.0.7 里好好的升到 4.1.0 之后status 1这个条件直接就 消失 了最终生成的查询语句是空对象{}相当于执行全表扫描。更坑的是它不报错、不抛异常就是静默地把条件丢掉了如果你没有完善的单元测试覆盖上线之后就是数据泄漏或者业务逻辑错乱。二、根因深挖Criteria 的内部机制到底改了什么要搞懂这个问题得先明白Criteria这个类的内部数据结构。2.1 Criteria 的本质一个链式条件容器Criteria内部核心就两个东西key当前正在操作的字段名比如statuscriteriaChain一个 List存放多个子 Criteria用于组合多字段条件当你写Criteria.where(status).is(1).and(name).is(zhangsan)时内部其实是构建了一条条件链最终生成{ status: 1, name: zhangsan }这样的查询文档。2.2 4.0 版本宽松 的 and 方法实现在 4.0 及更早版本中new Criteria()会创建一个空壳对象——key 为 nullcriteriaChain 为空列表。当你调用.and(status)时旧版本的逻辑是如果当前 Criteria 没有 key空对象那就把这个 key 设为自己的 key相当于把自己从空对象 激活 成一个真实的条件对象。所以new Criteria().and(status).is(1)虽然写法不标准但碰巧能工作因为and()顺手帮你把第一个字段给初始化了。2.3 4.1 版本严格的语义修正到了 4.1 版本Spring 团队对and()方法做了语义修正and()方法的本职是 追加下一个字段条件而不是 初始化第一个字段。也就是说and()从设计上来讲就应该用在已经有至少一个条件的 Criteria 后面用来继续拼接第二个、第三个字段。如果你拿一个空的 Criteria 对象直接调用and()从语义上讲是不成立的。所以 4.1 里改了逻辑如果当前 Criteria 本身没有 key空对象调用and(key)不会再给当前对象赋值 key而是创建一个新的 Criteria 加入到 criteriaChain 中但由于各种内部判断逻辑的连锁反应最终在getCriteriaObject()序列化时这个条件被 优化 掉了导致最终输出为空说白话就是以前是 容错式 设计你写错了也能跑现在是 严格式 设计不符合 API 设计意图的写法就直接不给你生效了。三、解决方案三种正确写法任选其一知道了原因改起来就很简单。这里给大家三种标准写法都能在 4.1.0 中正常工作。方案一使用 Criteria.where () 静态方法最推荐这是官方最推荐的标准写法也是语义最清晰的方式// 第一个条件用 where 开头后续用 and 拼接 Criteria criteria Criteria.where(status).is(1); // 如果还有其他条件继续 .and(xxx).is(yyy) Query query Query.query(criteria);这是最符合 API 设计意图的写法可读性也最好强烈建议统一成这种风格。方案二动态查询场景下用 andOperator如果是动态拼接多个条件比如前端传什么字段就拼什么用andOperator是最稳妥的ListCriteria criteriaList new ArrayList(); if (status ! null) { criteriaList.add(Criteria.where(status).is(status)); } if (name ! null) { criteriaList.add(Criteria.where(name).is(name)); } Criteria criteria new Criteria().andOperator(criteriaList.toArray(Criteria[]::new)); Query query Query.query(criteria);这种方式每个条件都是独立的 Criteria 对象通过andOperator显式组合语义明确完全不受版本变更影响。方案三第一个条件直接用有参构造如果你就是喜欢先 new 对象再赋值可以用带 key 的构造函数// 直接指定第一个字段 Criteria criteria new Criteria(status); criteria.is(1); Query query Query.query(criteria);这种也能正常工作本质和Criteria.where()是一样的。四、延伸思考为什么会有这个破坏性变更可能有人会说Spring 团队没事干吗好好的为啥要改这个站在框架设计者的角度其实是合理的语义一致性and就是 并且 的意思前面得先有东西才能 并且。空对象调用and在语义上是不成立的。减少歧义旧版本的容错设计其实掩盖了很多错误用法很多人根本不知道new Criteria()和Criteria.where()的区别。内部重构需要4.1 版本对 Criteria 的序列化逻辑做了大量重构为了支持更复杂的查询表达式必须收紧边界。只不过这种静默失效的方式确实不够友好 —— 如果直接抛出异常提示 请使用 Criteria.where () 初始化第一个条件开发者反而能更快定位问题。五、升级避坑建议最后给大家几点 Spring Boot 版本升级时的实操建议MongoDB 模块重点回归Spring Data MongoDB 每个大版本的破坏性变更都不少升级时一定要重点跑查询相关的单测。统一代码风格项目里所有 Criteria 构建统一用Criteria.where()开头禁止new Criteria()直接接.and()的写法。打印查询语句关键业务的查询建议在 debug 日志里打印出最终生成的 query 对象方便排查问题。不要依赖容错行为框架的 好心容错 往往是埋雷的地方尽量按照官方标准用法来写别钻空子。结语这个问题说大不大说小不小 —— 没踩到时觉得无所谓踩到了就是线上故障。本质上这是一个 非标准用法 撞上 版本严格化 的典型案例。Spring 生态庞大每个小版本都可能藏着这种静默变更升级的时候多留个心眼总没错。如果你的项目也准备升 Spring Boot 4.1赶紧搜一下代码里有没有new Criteria().and这种写法趁早改掉省得上线之后踩坑。如果觉得文章有帮助欢迎点赞收藏也欢迎在评论区交流你升级时遇到的奇葩问题。