关于fastjson的知识又增加了

本周有一个需求,需要调用第三方的阿里云接口,对方要求的协议参数,必须首字母大写。而通常情况下,我们定义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}

总结

  1. 可以使用SerializeConfig和ParserConfig在执行操作的时候,定制行为。但是一定要注意,在生产环境,将其设置为singleton。
  2. 可以使用@JSONField进行字段级别的序列化和反序列化配置。
  3. 可以使用@JSONType进行类级别的序列化和反序列化配置,优先级低于@JSONField
  4. 默认情况下,在进行反序列化时,FastJson会进行smartMatch,会屏蔽大小写,下划线和中划线的差异。也就是userName等价于UserName等价于user-name。可以通过序列化时,指定Feature.DisableFieldSmartMatch关闭此特性.
上一篇:SSLOJ2299护卫队&&P1594


下一篇:错误集