本周有一个需求,需要调用第三方的阿里云接口,对方要求的协议参数,必须首字母大写。而通常情况下,我们定义Bean的时候,不会直接将变量名设置为大写开头,这样不符合编码规范,那有什么办法可以将首字母序列化为大写的字符串,作为请求参数传递呢?这里主要通过FastJson的一些定制化行为,完成了该类需求。同时,在这个过程中,顺便阅读了一些fastjson的源码,特此记录一下。
序列化
@Data
public static class Model {
private int userId;
private String userName;
}
使用代码验证一下默认的序列化行为。
Model model = new Model();
model.userId = 1001;
model.userName = "test";
System.out.println(JSON.toJSONString(model));
输出结果:
{"userId":1001,"userName":"test"}
可以看到默认的序列化行为是驼峰形式。
那如果要实现首字母大写的序列化形式,要如何操作呢?
方案1 序列化时指定配置
Model model = new Model();
model.userId = 1001;
model.userName = "test";
// 生产环境中,config需要设置为singleton处理,不然会存在性能问题
SerializeConfig serializeConfig = new SerializeConfig();
serializeConfig.propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
String text = JSON.toJSONString(model, serializeConfig, SerializerFeature.SortField);
对于PropertyNamingStrategy的行为可以参照FastJson的issue https://github.com/alibaba/fastjson/wiki/PropertyNamingStrategy_cn
输出结果:
{"UserId":1001,"UserName":"test"}
方案2 使用JSONField注解指定字段级别的配置
@Data
public static class ModelOne {
@JSONField(name = "UserId")
private int userId;
@JSONField(name = "UserName")
private String userName;
}
测试代码:
ModelOne model = new ModelOne();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
输出结果:
{"UserId":1001,"UserName":"test"}
方案3 使用JSONType注解指定类级别的配置
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelTwo {
private int userId;
private String userName;
}
测试代码:
ModelTwo model = new ModelTwo();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
输出结果:
{"UserId":1001,"UserName":"test"}
当JSONType和JSONField并行使用时的行为
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelThree {
private int userId;
@JSONField(name = "userName")
private String userName;
}
测试代码:
ModelThree model = new ModelThree();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
输出示例:
{"UserId":1001,"userName":"test"}
可以看到,如果两者共同使用时,会以字段上的JSONField为主。
反序列化
看了序列化之后,那反序列化是否类似呢。
我们的输入字符串均是:
{\"UserId\":1001, \"UserName\":\"test\"}
断言验证:
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
默认反序列化
@Data
public static class Model {
private int userId;
private String userName;
}
Model model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", Model.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
反序列化时指定配置
@Data
public static class ModelZero {
private int userId;
private String userName;
}
// 生成环境,需要设置为singleton,不然会存在性能问题
ParserConfig parserConfig = new ParserConfig();
parserConfig.propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
Model model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", Model.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
// 测试通过
使用了JSONField配置后,进行反序列化
@Data
public static class ModelOne {
@JSONField(name = "UserId")
private int userId;
@JSONField(name = "UserName")
private String userName;
}
ModelOne model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelOne.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
使用JSONType配置后,进行反序列化
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelTwo {
private int userId;
private String userName;
}
ModelTwo model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelTwo.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
配合使用JSONType和JSONField
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelThree {
private int userId;
@JSONField(name = "userName")
private String userName;
}
ModelThree model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelThree.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
通过跑Test case发现,上述的反序列化验证代码,竟然全部通过了,对于使用了单独使用了JSONField和单独使用了JSONType的行为非常容易理解,因为输入字符串和自己的指定是一致的。但是为什么默认情况下,包括通过@JSONField将字段配置改为小写的情况下,还可以正常序列化呢?
关于FastJson的smartMatch
上述问题的核心在于FastJson的smartMatch,也就是说fastJson的智能检测,将UserId映射到了userId,UserName映射到了userName。此时就需要去源码中进行验证了,这一段的核心源码在JavaBeanDeserializer 方法 public FieldDeserializer smartMatch(String key, int[] setFlags)中。
对该段源码解析一下:
public FieldDeserializer smartMatch(String key, int[] setFlags) {
// key: 输入的字段名称,比如UserName或者UserId
if (key == null) {
return null;
}
// 先从正常的字段反序列化器中寻找,正常的字段反序列化器中存在的是以 uesrId和userName驼峰式命名的,所以在当输入是UserId或者UserName首字母大写时,无法找到正确的反序列化器。
FieldDeserializer fieldDeserializer = getFieldDeserializer(key, setFlags);
if (fieldDeserializer == null) {
if (this.smartMatchHashArray == null) {
// 基于已有的正常的序列化器,生成一个智能匹配array
long[] hashArray = new long[sortedFieldDeserializers.length];
for (int i = 0; i < sortedFieldDeserializers.length; i++) {
hashArray[i] = sortedFieldDeserializers[i].fieldInfo.nameHashCode;
}
Arrays.sort(hashArray);
this.smartMatchHashArray = hashArray;
}
// smartMatchHashArrayMapping
// 这里是智能匹配的核心代码,先根据下面的TypeUtils.fnval_64_lower计算key的hash值
//public static long fnv1a_64_lower(String key){
// long hashCode = 0xcbf29ce484222325L;
// for(int i = 0; i < key.length(); ++i){
// char ch = key.charAt(i);
// 这里是关键,这里会将大写字母转为小写字母,所以userName和UserName计算得到的hash值是一样的
// if(ch >= 'A' && ch <= 'Z'){
// ch = (char) (ch + 32);
// }
// hashCode ^= ch;
// hashCode *= 0x100000001b3L;
// }
// return hashCode;
// }
// 先将大写字母转为小写计算
long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
// 在已有的反序列化器的key中寻找对应的位置
int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
if (pos < 0) {
// 如果没有找到,则需要看一下是否存在下划线或者中划线
// public static long fnv1a_64_extract(String key){
// long hashCode = 0xcbf29ce484222325L;
// for(int i = 0; i < key.length(); ++i){
// char ch = key.charAt(i);
// 计算的时候不考虑中划线或者下划线
// if(ch == '_' || ch == '-'){
// continue;
// }
// 大写字母转为小写字母
// if(ch >= 'A' && ch <= 'Z'){
// ch = (char) (ch + 32);
// }
// hashCode ^= ch;
// hashCode *= 0x100000001b3L;
// }
// return hashCode;
//}
long smartKeyHash1 = TypeUtils.fnv1a_64_extract(key);
pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash1);
}
boolean is = false;
if (pos < 0 && (is = key.startsWith("is"))) {
// 对于boolean类型的,通常的get方法是以is开头的,需要特殊处理
smartKeyHash = TypeUtils.fnv1a_64_extract(key.substring(2));
pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
}
if (pos >= 0) {
// 如果根据hash找到了正常反序列化器的hash位置
if (smartMatchHashArrayMapping == null) {
// 下面的逻辑,主要是获取每个序列化器的位置
short[] mapping = new short[smartMatchHashArray.length];
Arrays.fill(mapping, (short) -1);
for (int i = 0; i < sortedFieldDeserializers.length; i++) {
// 对所有的序列化器遍历
int p = Arrays.binarySearch(smartMatchHashArray, sortedFieldDeserializers[i].fieldInfo.nameHashCode);
if (p >= 0) {
// 赋值,p位置对应的key hash,应该使用i位置的反序列化器
mapping[p] = (short) i;
}
}
smartMatchHashArrayMapping = mapping;
}
int deserIndex = smartMatchHashArrayMapping[pos];
if (deserIndex != -1) {
if (!isSetFlag(deserIndex, setFlags)) {
// 对序列化器赋值
fieldDeserializer = sortedFieldDeserializers[deserIndex];
}
}
}
if (fieldDeserializer != null) {
FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
// 这里很关键,如果设置了disalbeFieldSmartMatch,就直接返回null了
if ((fieldInfo.parserFeatures & Feature.DisableFieldSmartMatch.mask) != 0) {
return null;
}
Class fieldClass = fieldInfo.fieldClass;
if (is && (fieldClass != boolean.class && fieldClass != Boolean.class)) {
// 如果是以is,但是类型又不是boolean类型,返回null
fieldDeserializer = null;
}
}
}
return fieldDeserializer;
}
分析完源码,我们可以通过设置Feature.DisableFieldSmartMatch来看看是否可以解决我们的疑问。
我们对默认的行为进行验证,另外一种情况类似
ModelZero model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelZero.class, Feature.DisableFieldSmartMatch);
System.out.println(JSON.toJSONString(model2));
输出结果:
// userName没有映射上,所以是null,没有输出。
// userId也没有映射上,是int的默认值,为0
{"userId":0}
总结
- 可以使用SerializeConfig和ParserConfig在执行操作的时候,定制行为。但是一定要注意,在生产环境,将其设置为singleton。
- 可以使用@JSONField进行字段级别的序列化和反序列化配置。
- 可以使用@JSONType进行类级别的序列化和反序列化配置,优先级低于@JSONField
- 默认情况下,在进行反序列化时,FastJson会进行smartMatch,会屏蔽大小写,下划线和中划线的差异。也就是userName等价于UserName等价于user-name。可以通过序列化时,指定Feature.DisableFieldSmartMatch关闭此特性.