Spring Boot 接口返回 Bean 时字段名“变形”的深度剖析与解决方案

问题:定义 private String aBcc;,返回 JSON 却变成了 "abcc",更奇怪的是加上 @JsonProperty 后竟然同时出现了 abccaBcc 两个字段。

本文将深入剖析这一现象背后的技术原理(JavaBean 规范、Lombok、Jackson),并给出多种可靠解决方案,帮你彻底告别此类问题。

一、问题重现

假设我们有一个 Spring Boot 项目,定义了一个响应类:

java

kotlin 复制代码
import lombok.Data;

@Data
public class MyResponse {
    private String aBcc;  // 注意:首字母小写,第二个字母大写
    // 其他字段...
}

在 Controller 中返回该对象:

java

java 复制代码
@RestController
public class TestController {
    @GetMapping("/test")
    public MyResponse test() {
        MyResponse resp = new MyResponse();
        resp.setABcc("hello");  // Lombok 生成的 setter 方法名为 setABcc()
        return resp;
    }
}

前端收到的 JSON 却是:

json

json 复制代码
{
    "abcc": "hello"
}

我们期望的是 aBcc,但实际得到了 abcc。为了强制指定,我们在字段上添加 @JsonProperty("aBcc")

java

kotlin 复制代码
@Data
public class MyResponse {
    @JsonProperty("aBcc")
    private String aBcc;
}

结果返回的 JSON 变成了:

json

json 复制代码
{
    "abcc": "hello",
    "aBcc": "hello"
}

竟然同时出现了两个属性!这是怎么回事?


二、原因深度分析

要理解这个问题,需要捋清三个关键角色的行为:JavaBean 规范LombokJackson

1. JavaBean 规范对属性命名的规定

JavaBean 规范定义:属性名是通过 getter/setter 方法名推导出来的。规则是:

  • 去掉方法名的 getset 前缀。
  • 将剩余部分的首字母改为小写。
  • 如果剩余部分的前两个字母都是大写,则保持不变(特殊情况)。

例如:

  • getName() → 去掉 getName → 首字母小写 → name
  • getURL() → 去掉 getURL → 前两个字母都是大写,保持不变 → URL
  • getaBcc() → 去掉 getaBcc → 首字母已经是小写,保持不变 → aBcc
  • getABcc() → 去掉 getABcc → 前两个字母 AB 都是大写,保持不变 → ABcc(这不符合常规认知)

但大多数 JSON 库(包括 Jackson)在实现时,对上述规则做了更符合直觉 的处理:如果去掉前缀后首字母大写,但第二个字母小写,则会将首字母小写。然而对于 ABcc,它们会直接转为小写?实际上 Jackson 内部的命名转换逻辑比较复杂,但最终结果通常是 abcc

关键点 :规范的 getter 方法名对于字段 aBcc 应该是 getaBcc()(注意第二个字母是小写 a),而不是 getABcc()

2. Lombok 生成的 getter/setter

当我们在类上使用 @Data 注解时,Lombok 会为所有字段生成标准的 getter/setter。它的生成规则是:

  • 对于字段名,直接首字母大写并拼接 get 前缀。
  • 对于 aBcc,首字母大写后变为 ABcc,再拼接 get 得到 getABcc()

Lombok 不会 去判断是否应该保持第二个字母的大小写,它只是机械地首字母大写。因此生成的 setter 是 setABcc()

不符合 JavaBean 规范(规范要求是 getaBcc())。

3. Jackson 的序列化过程

Spring Boot 默认使用 Jackson 作为 JSON 序列化工具。Jackson 在序列化一个对象时,会扫描该对象的所有"可访问"属性。它主要通过两种方式:

  • Getter 方法 :Jackson 会查找所有以 get 开头的方法,并依据 JavaBean 规范推导出属性名。
  • 字段(Field) :如果字段是 public 或者有 @JsonProperty 注解,Jackson 也会直接使用字段名作为属性名。

对于我们的场景

  • Lombok 生成了 getABcc() 方法。
  • Jackson 看到 getABcc(),去掉 get 得到 ABcc。按照其内部命名策略(通常是 PropertyNamingStrategy),它会将 ABcc 转换为 abcc(因为 Jackson 认为这是驼峰命名,会将连续大写字母转为小写,但具体实现细节不展开)。最终 JSON 键名为 abcc

这就是最初出现 abcc 的原因。

4. 引入 @JsonProperty 后为什么会出现双属性?

当我们在字段上添加 @JsonProperty("aBcc") 时:

  • 该注解让私有字段变得可序列化 ,Jackson 会直接使用注解指定的名称 aBcc 输出该字段的值。
  • 同时,因为 getABcc() 方法依然存在且是 public 的,Jackson 仍然会将其作为一个属性源,并按照默认规则生成键名 abcc
  • 两个来源都指向同一个值,最终 JSON 中便出现了两个同名异键的属性。

注意 :如果 Lombok 能够将字段上的 @JsonProperty 复制到它生成的 getter 方法上,那么 Jackson 在处理 getter 时也会使用 aBcc,两个来源会合并为一个,不会重复。但 Lombok 默认只复制有限的注解 (如 @NonNull),并不包括 @JsonProperty。除非我们主动配置 lombok.config 让 Lombok 复制该注解。


三、解决方案

根据项目需求,我们可以选择以下方案之一,均能彻底解决问题。

方案一:手动编写符合规范的 Getter(最直接,推荐)

放弃 Lombok 为该字段生成的 getter/setter,手动编写符合 JavaBean 规范的 getaBcc()setaBcc()

java

typescript 复制代码
@Data
public class MyResponse {
    private String aBcc;

    // 手动编写,注意方法名是 getaBcc(小写 a)
    public String getaBcc() {
        return aBcc;
    }

    public void setaBcc(String aBcc) {
        this.aBcc = aBcc;
    }
}

此时 Lombok 检测到已有手动编写的 getter/setter,便不会再自动生成。Jackson 看到 getaBcc(),去掉 get 后为 aBcc,首字母已小写,保持不变,JSON 输出为 aBcc。完美。

优点 :简单明确,无需额外配置,完全遵循 JavaBean 规范。

缺点:需要手动编写,略显繁琐(但仅针对少数特殊命名字段)。


方案二:修改字段命名(根本解决)

如果条件允许,直接修改字段名,使其符合常见的驼峰命名(第二个字母小写)或干脆改为其他命名。

java

kotlin 复制代码
@Data
public class MyResponse {
    private String abcc;  // 全部小写,或者 bcc
}

此时 Lombok 生成 getAbcc(),Jackson 推导出 abcc,两者一致。

优点 :从根源上杜绝此类问题,符合编码规范,一劳永逸。

缺点:如果字段名已在前后端约定好,修改成本较高。


方案三:配置 Lombok 复制 @JsonProperty 注解(保留 Lombok 自动化)

如果你希望继续使用 Lombok 自动生成所有 getter/setter,并且想用 @JsonProperty 显式指定序列化名称,那么可以配置 Lombok 让其将字段上的注解复制到生成的方法上。

在项目根目录(src/main 同级)或 src/main/java 目录下创建 lombok.config 文件,内容如下:

properties

kotlin 复制代码
# 让 Lombok 将 @JsonProperty 复制到生成的 getter/setter 方法上
lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty

然后在字段上添加 @JsonProperty("aBcc")

java

kotlin 复制代码
@Data
public class MyResponse {
    @JsonProperty("aBcc")
    private String aBcc;
}

此时 Lombok 生成的 getABcc() 方法上也会带上 @JsonProperty("aBcc")。Jackson 在处理方法时,会使用注解指定的名称 aBcc,而字段上的注解同样指定 aBcc,两者合并,最终 JSON 中只出现一个 aBcc 属性。

优点 :完全保留 Lombok 的简洁性,无需手写代码。

缺点:需要额外配置文件,且确保构建工具能识别(Maven/Gradle 均支持)。


方案四:全局配置 Jackson 的命名策略(不推荐)

如果你希望统一所有字段的命名规则,可以配置 Jackson 使用 PropertyNamingStrategy,例如 SNAKE_CASEKEBAB_CASE。但对于单一字段的特殊需求,这种全局配置灵活性不足,且会使所有字段都改变命名,通常不适用于本问题。


四、各方案对比与选择建议

方案 优点 缺点 适用场景
方案一(手动 getter) 符合规范,无额外配置,立即生效 需要手动编写少量代码 字段命名已定,且仅少数字段有此问题
方案二(改字段名) 最根本、最规范,彻底避免问题 可能涉及前后端接口变更 项目初期或前后端可协商修改时
方案三(Lombok 配置) 保持 Lombok 简洁,自动化程度高 需要配置文件,且需确保所有开发环境一致 大量使用 @JsonProperty 且希望保持代码简洁
方案四(全局命名策略) 统一全局命名风格 无法针对个别字段精细控制 仅当整个项目需要统一命名风格时使用

我的推荐 :如果字段数量少且命名已固定,方案一 最为稳妥;如果还未上线或可以调整,方案二 是长远之选;如果你的项目对代码简洁度要求极高且愿意增加配置,方案三也是一个优雅的选择。


五、总结

这个问题本质上是由 Lombok 对不规范字段名的机械处理Jackson 对 JavaBean 规范的严格遵循 之间的冲突引起的。关键点在于:

  • 字段 aBcc 的正确 getter 应为 getaBcc()
  • Lombok 生成了 getABcc(),导致 Jackson 推导出 abcc
  • 添加 @JsonProperty 时未配置 Lombok 复制注解,导致字段和 getter 被视为两个独立属性源,产生双属性。

通过理解这背后的机制,我们可以根据项目实际情况灵活选择解决方案,彻底避免此类"字段名变形"的问题。希望本文能帮助你扫清这一常见但令人困惑的障碍,让 Spring Boot 开发更加顺畅。


如果你有任何疑问或更好的实践,欢迎在评论区交流讨论!😊

(完)

相关推荐
步行cgn1 小时前
MyBatis 高级映射与延迟加载完全指南
java·tomcat·mybatis
thefool1122661 小时前
从中序与后序遍历序列构造二叉树
java
吴声子夜歌1 小时前
Java面试题——JVM(二)
java·开发语言·jvm
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
denglinqings1 小时前
Java开发从入门到精通所有课程
java·开发语言
星空2 小时前
maven镜像
java·maven
chuan.bai3 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法
SL_staff3 小时前
销售知识管理的断点分析与技术解法:从3天找话术到秒级检索
java·开源·github
荣码3 小时前
向量数据库选型:4种方案全测了,选错重来成本最高
java·python