1. 问题概述
在Java开发中,我们有时会遇到一些形式上类似驼峰命名但语义上不符合标准Java命名规范的属性命名,如fDeptCode
(使用单字母前缀)、iPhone
(品牌命名)等。这些命名虽然也是"以小写字母开头,后续单词首字母大写"的格式,但因为其首字母并非来自有实际含义的完整单词,所以不符合Java标准命名规范的语义要求。当使用Jackson进行JSON序列化和反序列化时,这类特殊命名的属性可能会遇到意外问题。
1.1 问题示例
考虑以下包含fDeptCode
属性的Java类:
import lombok.Data;
@Data
public class Department {
private String fDeptCode;
// lombok自动生成的getter/setter
public String getFDeptCode() {
return fDeptCode;
}
public void setFDeptCode(String fDeptCode) {
this.fDeptCode = fDeptCode;
}
}
当我们尝试将JSON字符串{"fDeptCode":"001"}
反序列化为Department
对象时,fDeptCode
属性没有被正确赋值(为null)。
1.2 问题原因概述
这个问题的关键在于Jackson处理getter/setter方法时的属性名推断机制:
- Jackson从
getFDeptCode()
方法推断出属性名为fdeptCode
(具体原因看2.1 属性收集的核心流程、3.1 属性名推断的关键代码、) - 但JSON中的字段名是
fDeptCode
- 导致无法正确匹配和赋值
1.3 解决方案
最简单直接的方法是使用@JsonProperty注解明确指定JSON字段名:
public class Department {
@JsonProperty("fDeptCode")
private String fDeptCode;
// getter/setter...
}
2. Jackson属性处理机制深入分析
Jackson使用POJOPropertiesCollector.collectAll()
收集和处理Java类的属性信息,这是理解问题的关键。下面是该方法的完整源码:
/**
* 内部方法,用于收集所有属性信息
*
* @since 2.6
*/
protected void collectAll() {
// 使用LinkedHashMap存储属性信息,保持插入顺序
LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();
// 第一阶段:收集基础数据
_addFields(props); // 收集类中的所有字段
_addMethods(props); // 收集getter/setter等方法
// 如果不是非静态内部类,收集构造函数和工厂方法
if (!_classDef.isNonStaticInnerClass()) {
_addCreators(props);
}
// 第二阶段:移除被@JsonIgnore注解标记的属性,
// 移除没有getter/setter方法的属性
_removeUnwantedProperties(props);
// 移除不需要的访问器(如只读/只写情况)
_removeUnwantedAccessor(props);
// 第三阶段:重命名剩余属性
_renameProperties(props);
// 第四阶段:添加可注入的属性,注意避免与创建器和常规属性重叠
_addInjectables(props);
// 第五阶段:合并注解,简化后续处理
for (POJOPropertyBuilder property : props.values()) {
property.mergeAnnotations(_forSerialization);
}
// 第六阶段:根据可见性排序(显式优先于隐式)
// 如果存在可见性差异,则每种成员类型(getter、setter等)只保留第一个
for (POJOPropertyBuilder property : props.values()) {
property.trimByVisibility();
}
// 第七阶段:应用自定义命名策略(如果适用)
PropertyNamingStrategy naming = _findNamingStrategy();
if (naming != null) {
_renameUsing(props, naming);
}
if (_config.isEnabled(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME)) {
_renameWithWrappers(props);
}
// 最后阶段:对属性进行排序
_sortProperties(props);
// 保存处理完的属性并标记收集完成
_properties = props;
_collected = true;
}
2.1 属性收集的核心流程
以下是Jackson属性收集的简化流程:
1. 收集字段信息 (_addFields)
2. 收集getter/setter方法 (_addMethods)
3. 移除不需要的属性 (_removeUnwantedProperties)
2.2 详细处理过程分析
针对Department
类的处理过程:
-
字段收集阶段:
_addFields
方法收集字段,创建key为fDeptCode
的POJOPropertyBuilder
-
方法收集阶段:
_addMethods
方法处理getter/setter- 从
getFDeptCode()
推断属性名为fdeptCode
(注意首字母小写且D也变小写) - 由于找不到名为
fdeptCode
的现有属性,创建新的POJOPropertyBuilder
- 结果:props中存在两个实体,key分别为
fDeptCode
和fdeptCode
-
属性清理阶段:
_removeUnwantedProperties
移除没有getter/setter的属性fDeptCode
对应的POJOPropertyBuilder
没有绑定getter/setter,被移除- 只保留了
fdeptCode
对应的POJOPropertyBuilder
-
反序列化结果:
- JSON中只有
fDeptCode
字段,没有fdeptCode
字段 - Jackson只能识别
fdeptCode
属性,导致反序列化后fDeptCode
为null
- JSON中只有
3. 属性名推断机制剖析
问题的核心在于Jackson如何从getter/setter方法名推断属性名。
3.1 属性名推断的关键代码
Jackson使用DefaultAccessorNamingStrategy
处理属性名推断:
/**
* 处理属性名称的方法,主要用于将方法名或字段名转换为属性名
* 例如:将 "UserName" 转换为 "userName",将 "URL" 转换为 "url"
*
* @param basename getter/setter方法的名称,不包含前缀("get"/"is"/"set")
* @param offset 起始处理位置的偏移量
* @return 处理后的属性名称,如果basename为空则返回null
*/
protected static String legacyManglePropertyName(final String basename, final int offset)
{
// 获取字符串的总长度
final int end = basename.length();
// 如果偏移量等于字符串长度,说明是空字符串,返回null
if (end == offset) {
return null;
}
// 获取偏移位置的第一个字符,并计算其小写形式
char c = basename.charAt(offset);
char d = Character.toLowerCase(c);
// 如果第一个字符已经是小写,直接返回从offset开始的子字符串
if (c == d) {
return basename.substring(offset);
}
// 处理首字母大写的情况
// 创建StringBuilder来存储结果,预分配合适的容量以提高性能
StringBuilder sb = new StringBuilder(end - offset);
// 添加第一个字符的小写形式
sb.append(d);
// 处理剩余的字符:将连续的大写字母全部转为小写,直到遇到第一个小写字母。
int i = offset+1;
for (; i < end; ++i) {
c = basename.charAt(i);
d = Character.toLowerCase(c);
// 如果当前字符已经是小写,说明后面的字符都不需要处理
// 直接将剩余的所有字符添加到结果中
if (c == d) {
sb.append(basename, i, end);
break;
}
// 如果当前字符是大写,将其转换为小写后添加
sb.append(d);
}
return sb.toString();
}
3.2 问题根源
对于方法名getFDeptCode
:
- 调用legacyManglePropertyName方法时,basename是FDeptCode,offset是3
- 因为首字母是大写字母,所以会将第一个小写字母(e)前的大写字母们都转为小写字母,
最终得到的属性名是
fdeptCode
这是特殊命名模式(如fDeptCode
)出现问题的根本原因:Jackson会将连续的大写字母全部转为小写,直到遇到第一个小写字母。
4. 解决方案
针对这个问题,有几种有效的解决方案:
4.1 使用@JsonProperty注解(最推荐)
最简单直接的方法是使用@JsonProperty
注解明确指定JSON字段名:
public class Department {
@JsonProperty("fDeptCode")
private String fDeptCode;
// getter/setter...
}
4.2 自定义命名策略
可以实现自定义的PropertyNamingStrategy
来处理特殊命名模式:
public class CustomNamingStrategy extends PropertyNamingStrategy {
@Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
// 自定义处理特殊命名模式的逻辑
return customProcessPropertyName(defaultName);
}
private String customProcessPropertyName(String name) {
// 根据需要处理属性名...
}
}
4.3 配置Jackson选项
可以调整Jackson的配置选项来改变默认行为:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
5. 最佳实践建议
为避免特殊命名模式引起的问题,建议:
- 一致的命名约定:尽量使用标准的Java命名约定(驼峰命名法)
- 显式注解:对于特殊命名的属性,使用
@JsonProperty
注解明确指定
6. 总结
Jackson在处理特殊命名模式的属性时,由于其属性名推断机制的特点,可能导致JSON字段与Java属性无法正确映射。通过理解Jackson内部的属性处理机制,特别是属性名推断过程,我们可以有针对性地解决这类问题,确保JSON序列化和反序列化的正确性。