本周有一个需求,需要调用第三方的阿里云接口,对方要求的协议参数,必须首字母大写。而通常情况下,我们定义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"}
可以看到默认的序列化行为是驼峰形式。
那如果要实现首字母大写的序列化形式,要如何操作呢?
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"}
@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"}
@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"}
@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); // 测试通过
@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);
@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);
@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的智能检测,将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}