2025-12-25
人生代代无穷已,江月年年只相似。 --- 《春江花月夜》 · 唐代 张若虚
Details
全文
春江潮水连海平,海上明月共 潮生。滟滟随波千万里,何处春江无月明!
江流宛转绕芳甸,月照花林皆似霰。
空里流霜不觉飞,汀上白沙看不见。
江天一色无纤尘,皎皎空中孤月轮。
江畔何人初见月,江月何年初照人?
人生代代无穷已,江月年年只相似。
不知江月待何人,但见长江送流水。
白云一片去悠悠,青枫浦上不胜愁。
谁家今夜扁舟子,何处相思明月楼?
可怜楼上月徘徊,应照离人妆镜台。
玉户帘中卷不去,捣衣砧上拂还来。
此时相望不相闻,愿逐月华流照君。
鸿雁长飞光不度,鱼龙潜跃水成文。
昨夜闲潭梦落花,可怜春半不还家。
江水流春去欲尽,江潭落月复西斜。
斜月沉沉藏海雾,碣石潇湘无限路。
不知乘月几人归,落月摇情满江树。
Feign + Jackson + Java Record
null 字段在 JSON 序列化中未被忽略的问题分析与解决
一、问题背景
在使用 Java record 作为 Feign 请求体,并通过 Jackson 进行 JSON 序列化时,期望做到:
当 record 中某个字段为
null时,不将该字段写入 JSON
已知条件:
-
使用
@JsonInclude(JsonInclude.Include.NON_NULL) -
Feign Client 发送请求
-
record 中存在嵌套 record(
BareRequest.Condition)
但在 Feign DEBUG 日志中仍然看到:
"fromSource": null
二、问题复现代码(简化)
1️⃣ record 定义
@JsonInclude(JsonInclude.Include.NON_NULL)
public record BareRequest(
int mode,
String className,
boolean queryLike,
List<Condition> conditions
) {
public record Condition(
String column,
int option,
Object value,
Integer fromSource
) {}
}
2️⃣ 构造请求对象
var conditions = List.of(
new BareRequest.Condition("cmdb_approval_status", 2, "YES", null),
new BareRequest.Condition("operational_status", 7, "[Decommissioned]", 1),
new BareRequest.Condition("server_type", 2, "PM-BareMetal", null),
new BareRequest.Condition("support_group", 2, "XPO-", null)
);
var request = new BareRequest(0, "All", true, conditions);
client.search(1, 3000, request);
3️⃣ 实际序列化日志(问题现象)
{
"column": "cmdb_approval_status",
"option": 2,
"value": "YES",
"fromSource": null
}
三、问题根因分析(关键结论)
✅ 结论一:@JsonInclude 不会向下传递到嵌套类型
-
@JsonInclude(NON_NULL)只对当前声明的类型生效 -
不会自动作用于:
-
内部 record
-
List 中的元素类型
-
子对象
-
因此:
-
BareRequest上的注解 对BareRequest.Condition无效 -
fromSource属于Condition,仍按默认规则序列化
这是 Jackson 的既定行为,不是 Feign 的问题,也不是 record 的问题
四、正确解决方案
方案一(最推荐):给嵌套 record 也加注解
@JsonInclude(JsonInclude.Include.NON_NULL)
public record BareRequest(
int mode,
String className,
boolean queryLike,
List<Condition> conditions
) {
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Condition(
String column,
int option,
Object value,
Integer fromSource
) {}
}
✅ 效果:
fromSource == null 时,字段不会出现在 JSON 中
方案二(更精确): 只控制单个字段
public record Condition(
String column,
int option,
Object value,
@JsonInclude(JsonInclude.Include.NON_NULL)
Integer fromSource
) {}
适用场景:
-
只希望忽略某一个 nullable 字段
-
其它字段仍保留默认行为
方案三(全局方案,需谨慎)
spring:
jackson:
default-property-inclusion: non_null
⚠️ 注意:
-
会影响 整个应用的 JSON 序列化行为
-
有些 API 可能依 赖
"field": null作为语义 -
一般不推荐直接全局开启
五、Feign 日志为何“暴露了这个问题”
Feign 在 loggerLevel = FULL 时:
-
使用 最终 ObjectMapper 序列化结果
-
不会做任何二次处理
-
日志内容 = 实际发送的 HTTP Body
因此:
Feign 日志是判断 Jackson 配置是否生效的最可靠依据
六、一个容易被忽略的附带问题(重要)
当前日志中有一条 condition:
"value": "[Decommissioned]"
这在 JSON 语义上是:
-
❌ 字符串
-
❌ 不是数组
如果后端期望的是:
"value": ["Decommissioned"]
那么正确的 Java 构造方式应是:
new BareRequest.Condition(
"operational_status",
7,
List.of("Decommissioned"),
1
);
否则可能导致:
-
条件失效
-
被当成普通字符串匹配
-
查询结果异常
该问题与 null 忽略 无关,但在 DSL / 查询场景中非常致命。
七、最终总结(可直接记住的规则)
-
@JsonInclude(NON_NULL)不会自动作用于嵌套 record -
List 元素、内部 record、子对象都需要 单独声明
-
Feign 只是“搬运工”,真正的规则在 Jackson
-
FULL 日志 = 最真实的请求体
-
Object value要警惕“伪数组字符串”问题
推荐实践(工程级)
-
DTO / 请求体 record
- 顶层 + 嵌套层都显式加
@JsonInclude
- 顶层 + 嵌套层都显式加
-
Feign 调试阶段
- 使用 FULL
-
生产
- 降级到 BASIC / HEADERS
-
查询 DSL
- value 类型尽量保持 JSON 语义一致(String ≠ List)
这套结论在 Spring Boot 3.x + Java 21 + OpenFeign + Jackson 环境下是稳定成立的。
Jackson 升级失败问题学习文档(Gradle + Spring Boot 2.7 + 企业 BOM 场景)
一、问题背景
项目环境与特征:
-
Gradle 多模块工程
-
Spring Boot 2.7.10
-
Java 21
-
使用 io.spring.dependency-management 插件
-
引入企业内部 BOM:
com.xxx.xx:xx-dependencies -
需要升级 Jackson 以解决:
-
record在 Feign / Jackson 反序列化时退化为反射set final field -
导致
IllegalAccessException
-
目标:
-
将 Jackson 从 2.13.5 升级到 2.20.1
-
保证:
-
依赖解析成功
-
构建、codegen、运行均可用
-
二、现象与核心错误
1. Jackson 版本始终被锁定在 2.13.5
即使已经在项目中引入:
mavenBom "com.fasterxml.jackson:jackson-bom:2.20.1"
dependencyInsight 仍显示:
com.fasterxml.jackson.core:jackson-databind:2.13.5
Selection reasons:
- Selected by rule
- By constraint
说明:
-
Jackson 版本不是由普通依赖冲突决定
-
而是被 dependency-management 的 BOM / 约束强制锁定
2. 尝试升级后构建失败
在成功“解锁版本管理”并让依赖指向 2.20.1 后,构建在 codegen 阶段失败:
Could not find com.fasterxml.jackson.core:jackson-annotations:2.20.1
关键信息:
-
错误来自
compileCodegenJava -
依赖链明确指向:
-
jackson-datatype-jsr310:2.20.1 -
jackson-bom:2.20.1
-
-
仓库中 无法解析
jackson-annotations:2.20.1
三、根因分析(分层)
层级 1:为什么 Jackson 一直是 2.13.5?
原因是 依赖管理层锁版本,而不是某个库“顺带带进来”。
关键配置:
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
imports {
mavenBom "com.xxx.xx:xx-dependencies:${version}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
结论:
-
xx-dependenciesBOM 内部 明确管理了 Jackson 版本(2.13.5) -
dependency-management插件生成 constraints / rules -
普通的:
-
implementation(...) -
platform(...) -
enforcedPlatform(...)
无法覆盖该层级的锁定
-
层级 2:为什么导入 jackson-bom:2.20.1 没生效?
即使导入顺序在后:
mavenBom "com.fasterxml.jackson:jackson-bom:2.20.1"
仍然无效,说明:
-
xframe BOM 中可能使用了:
-
dependencyManagement.dependencies -
或内部 plugin 注入的 version rule
-
-
Jackson 版本被 强制约束,不是普通 BOM 可覆盖级别
层级 3:为什么升级到 2.20.1 后直接报 “找不到包”?
错误本质不是版本冲突,而是:
仓库中不存在 / 不允许拉取 jackson-annotations:2.20.1
项目仓库结构:
repositories {
mavenLocal()
mavenCentral()
}
在企业环境中:
-
Maven Central 通常被 Nexus 私服代理
-
新版本依赖必须:
-
已被私服同步
-
或在白名单中
-
-
否则即使声明了
mavenCentral()也无法真正访问
结论:
-
Jackson 2.20.1 在当前私服中不可用
-
Gradle 无法下载
jackson-annotations:2.20.1 -
因此构建失败
四、关键结论总结(重要)
结论 1:不能靠 exclude 解决 Jackson 锁版本问题
-
exclude只能移除 传递依赖 -
无法移除:
-
dependency-management生成的 constraints -
插件注入的 version rule
-
-
在本项目中:无效
结论 2:enforcedPlatform 对 dependency-management 无效
-
enforcedPlatform是 Gradle 依赖解析层 -
dependency-management是 更高优先级的版本管理层 -
两者冲突时:
- dependency-management 胜出
结论 3:升级 Jackson ≠ 只改版本号
升级 Jackson 在企业项目中,必须同时满足:
-
版本锁定解除
-
私服中存在对应制品
-
所有子模块 / codegen / runtime 都能解析
缺一不可。
五、可行的现实解决路径
路径 A(最稳妥,短期推荐)
-
继续使用 Jackson 2.13.5
-
放弃在当前工程中使用
record作为 Feign DTO -
改用普通 POJO(无
final字段)
优点:
-
不改基础设施
-
构建、运行稳定
-
符合当前 xx + Boot 2.7 生态
路径 B(中期折中)
-
与基础设施团队确认:
- 私服是否已有 Jackson 2.18.x(LTS)
-
若可用:
- 在
dependencyManagement.dependencies中 逐个覆盖 Jackson 模块版本
- 在
-
避免使用 2.20.x 这种“非常新”的版本
路径 C(长期根治)
-
升级到 Spring Boot 3.x
-
配套升级:
-
Spring Cloud
-
xx 依赖
-
-
使用:
-
Jackson ≥ 2.15
-
record + constructor binding 成熟支持
-
六、关于最初的 record 反序列化异常的最终结论
你最早遇到的错误:
Can not set final boolean field ... stopDelivery
最终原因不是:
-
boolean vs Boolean
-
JsonNaming
-
Feign 配置
而是:
Jackson 2.13.5 在当前组合下未能正确走 record 构造器绑定,退化为反射写字段
这是 版本与生态匹配问题,不是代码写法 问题。
七、工程经验总结(可直接记住)
-
企业 BOM + dependency-management = 版本铁律
-
想升级核心库,必须先确认:
-
BOM
-
私服
-
-
Jackson / Netty / Log4j 这类基础库:
- “能不能拉到”比“新不新”更重要
-
Spring Boot 2.7 + Java 21 + record:
- 属于“勉强可用,但不顺配”的组合