跳到主要内容

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 / 查询场景中非常致命。


七、最终总结(可直接记住的规则)

  1. @JsonInclude(NON_NULL) 不会自动作用于嵌套 record

  2. List 元素、内部 record、子对象都需要 单独声明

  3. Feign 只是“搬运工”,真正的规则在 Jackson

  4. FULL 日志 = 最真实的请求体

  5. 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-dependencies BOM 内部 明确管理了 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 在企业项目中,必须同时满足:

  1. 版本锁定解除

  2. 私服中存在对应制品

  3. 所有子模块 / 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 构造器绑定,退化为反射写字段

这是 版本与生态匹配问题,不是代码写法问题。


七、工程经验总结(可直接记住)

  1. 企业 BOM + dependency-management = 版本铁律

  2. 想升级核心库,必须先确认:

    • BOM

    • 私服

  3. Jackson / Netty / Log4j 这类基础库:

    • “能不能拉到”比“新不新”更重要
  4. Spring Boot 2.7 + Java 21 + record:

    • 属于“勉强可用,但不顺配”的组合