问题:定义 private string abcc;,返回 json 却变成了 "abcc",更奇怪的是加上 @jsonproperty 后竟然同时出现了 abcc 和 abcc 两个字段。
本文将深入剖析这一现象背后的技术原理(javabean 规范、lombok、jackson),并给出多种可靠解决方案,帮你彻底告别此类问题。
一、问题重现
假设我们有一个 spring boot 项目,定义了一个响应类:
import lombok.data;
@data
public class myresponse {
private string abcc; // 注意:首字母小写,第二个字母大写
// 其他字段...
}
在 controller 中返回该对象:
@restcontroller
public class testcontroller {
@getmapping("/test")
public myresponse test() {
myresponse resp = new myresponse();
resp.setabcc("hello"); // lombok 生成的 setter 方法名为 setabcc()
return resp;
}
}
前端收到的 json 却是:
{
"abcc": "hello"
}我们期望的是 abcc,但实际得到了 abcc。为了强制指定,我们在字段上添加 @jsonproperty("abcc"):
@data
public class myresponse {
@jsonproperty("abcc")
private string abcc;
}
结果返回的 json 变成了:
{
"abcc": "hello",
"abcc": "hello"
}竟然同时出现了两个属性!这是怎么回事?
二、原因深度分析
要理解这个问题,需要捋清三个关键角色的行为:javabean 规范、lombok、jackson。
1. javabean 规范对属性命名的规定
javabean 规范定义:属性名是通过 getter/setter 方法名推导出来的。规则是:
- 去掉方法名的
get或set前缀。 - 将剩余部分的首字母改为小写。
- 如果剩余部分的前两个字母都是大写,则保持不变(特殊情况)。
例如:
getname()→ 去掉get→name→ 首字母小写 →namegeturl()→ 去掉get→url→ 前两个字母都是大写,保持不变 →urlgetabcc()→ 去掉get→abcc→ 首字母已经是小写,保持不变 →abccgetabcc()→ 去掉get→abcc→ 前两个字母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()。
@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 规范。
- 缺点:需要手动编写,略显繁琐(但仅针对少数特殊命名字段)。
方案二:修改字段命名(根本解决)
如果条件允许,直接修改字段名,使其符合常见的驼峰命名(第二个字母小写)或干脆改为其他命名。
@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 文件,内容如下:
# 让 lombok 将 @jsonproperty 复制到生成的 getter/setter 方法上 lombok.copyableannotations += com.fasterxml.jackson.annotation.jsonproperty
然后在字段上添加 @jsonproperty("abcc"):
@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_case 或 kebab_case。但对于单一字段的特殊需求,这种全局配置灵活性不足,且会使所有字段都改变命名,通常不适用于本问题。
四、各方案对比与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 方案一(手动 getter) | 符合规范,无额外配置,立即生效 | 需要手动编写少量代码 | 字段命名已定,且仅少数字段有此问题 |
| 方案二(改字段名) | 最根本、最规范,彻底避免问题 | 可能涉及前后端接口变更 | 项目初期或前后端可协商修改时 |
| 方案三(lombok 配置) | 保持 lombok 简洁,自动化程度高 | 需要配置文件,且需确保所有开发环境一致 | 大量使用 @jsonproperty 且希望保持代码简洁 |
| 方案四(全局命名策略) | 统一全局命名风格 | 无法针对个别字段精细控制 | 仅当整个项目需要统一命名风格时使用 |
我的推荐:如果字段数量少且命名已固定,方案一最为稳妥;如果还未上线或可以调整,方案二是长远之选;如果你的项目对代码简洁度要求极高且愿意增加配置,方案三也是一个优雅的选择。
五、总结
这个问题本质上是由 lombok 对不规范字段名的机械处理 与 jackson 对 javabean 规范的严格遵循 之间的冲突引起的。关键点在于:
- 字段
abcc的正确 getter 应为getabcc()。 - lombok 生成了
getabcc(),导致 jackson 推导出abcc。 - 添加
@jsonproperty时未配置 lombok 复制注解,导致字段和 getter 被视为两个独立属性源,产生双属性。
通过理解这背后的机制,我们可以根据项目实际情况灵活选择解决方案,彻底避免此类“字段名变形”的问题。希望本文能帮助你扫清这一常见但令人困惑的障碍,让 spring boot 开发更加顺畅。
到此这篇关于springboot接口返回bean时字段名变形的深度剖析与解决方案的文章就介绍到这了,更多相关springboot接口返回字段名变形内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论