阿里的fastjson

阿里的一些开源项目例如dubbo, druid, fastjson等在国内的影响力是蛮大的。今天谈下温少的fastjson, 它的流行源于它的快, 参考作者的谈fastjson内幕, 给出的测评是碾压jackson, 那时的jackson应该是1.x。https://www.iteye.com/blog/wenshao-1142031

笔者把fastjson整合到spring mvc 蛮多年, 当初还需要自己编写实现了泛型的 FastJsonHttpMessageConverter implements GenericHttpMessageConverter。 总体用法上觉得配置暴力些但使用还算简单, 全局的JSON属性, 基本都是静态方法调用, 传入一些Filter可过滤一些类的字段, 引用死循环简单设下属性也可避免。

而这一年来, fastjson被阿里云自身暴出不少漏洞,反串行化执行远程代码(网上有一些示范的攻击代码, 有兴趣同学自行搜索), 拒绝服务, 修复得算快, 但影响肯定是有的, 应该也不少公司在用了,但估计没能及时升级。笔者重新回来审视下json开发库的选择。

搜了些文章, 有些jdk 1.8之后String.substring实现的变化, fastjson的速度和jaskcon2是差不多的, 而fastjson内部用了ASM优化在大json串解析会消耗更多内存等等。 回头想想fastjson过程中也是碰到些问题, 一些特殊的json字段例如包含了/等, 默认开启了ASM, 即使在@JSONField设置了别名, 还是无法把json字符串转为对象, 必须禁止ASM, 例如

    //Without ASM by default
    private static FastJsonConfig fastJsonConfig = new FastJsonConfig();

    static {
        fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
        fastJsonConfig.getParserConfig().setAsmEnable(false);
    }

    public static FastJsonConfig getFastJsonConfig() {
        return fastJsonConfig;
    }

解析时传入FastJsonConfig即可。 Fastjson也有一些非标准的实现, 例如节点带入java class type, $引用等。

Spring MVC默认选择Jackson

现在回头看下Jackson, 参考下MappingJackson2HttpMessageConverter用法, 基本都是重用一个ObjectMapper是线程安全的, 构造ObjectMapper有点消耗, 所以网上有些性能评测每次调用就构造一个ObjectMapper是不大公平的。 基本json的设置都是绑定到ObjectMapper, 注册Filter, 模块等等, 扩展性较强, 每次写基本是构造新的ObjectWriter, 有一些可设置在ObjectWriter。 对象转为json串忽略字段,别名等基本比较依赖对象类使用注解@JsonProperty, @JsonIgnore, @JsonView等。

用的时候有时感觉不是太爽, 一个pojo类, 不同时候可能返回不同的json字段, 这样就需要在pojo加入很多jackson的注解JsonProperty, JsonView等, 侵入性有些强; 如果第三方的pojo无法加注解的, 虽然有ObjectMapper.addMixIn等方法绕过; 引用死循环需要手工指定 @JsonManagedReference和@JsonBackReference虽然合理但啰嗦些;总体API使用没fastjson舒服。 很多时候可能直接拼接为Map再转为json感觉还简单些。

那spring mvc为什么还是选择了jackson作为默认的json库呢? 主要的原因应该是jackson功能全面, 相对稳定, 可定制化一些。

(1) jackson包含了stream api, 有点类似 XML的SAX解析, 流读取可以省很多内存。 假设一个json文件很大, 只是需要统计里面的数据或部分数据, 用流api是十分高效的, 这应该是fastjson没有的。

套用网上的例子:

  public void testParser() throws Exception {
        String testStr = "{\"message\":\"Hello World!\",\"names\":[\"周杰伦\",\"王力宏\"]}";
        JsonParser p = factory.createParser(testStr);
 
        JsonToken t = p.nextToken();
        List<String> names = new ArrayList<String>();
        if ( t != JsonToken.START_OBJECT){
            System.out.println("Json格式不正确!");
            return;
        }
        while (t != JsonToken.END_OBJECT){
            if (t == JsonToken.FIELD_NAME && "message".equals(p.getCurrentName())){
                t =  p.nextToken();
                String message = p.getText();
                System.out.printf("My message to you is %s!\n", message);
            }
            if (t == JsonToken.FIELD_NAME && "names".equals(p.getCurrentName())){
                t = p.nextToken();
                while (t != JsonToken.END_ARRAY){
                    if (t == JsonToken.VALUE_STRING){
                        String name = p.getValueAsString();
                        names.add(name);
                    }
                    t = p.nextToken();
                }
            }
            t = p.nextToken();
        }
        System.out.println(names.toString());
        p.close();
}

(2)整个json类似XML DOM解析, 见代码

public static JsonNode parseNode(String text) {
   try {
      return objectMapper.readTree(text);
   }
   catch (Exception e) {
      e.printStackTrace();
      throw new RuntimeException("Failed to parseNode, " + e.toString(), e);
   }
}

JsonNode封装用起来就比fastjson啰嗦些了, fastjson解析为JSONObject和JSONArray好用一些。笔者也简单的把ObjectMapper解析为Map, 之后封装类似fastjson JSONObject和JSONArray, 可以参考 https://github.com/zealzeng/zen-framework/tree/master/zen-common/src/main/java/org/zenframework/util/json 下的JacksonUtils工具类parseObject, parseArray.

(3)Data binding转为对象就是 ObjectMapper处理的事情。

其实Jackson的CVE也不少,也是有一些反串行化,数据绑定有不少漏洞, 也是修修补补。 但是没办法spring mvc, spring boot, spring security里面json默认都是jackson处理, 如果不想多配置, jackson也将就着, 综合看它应该相对全面些稳些。 Fastjson等配置MessageConverter在spring 5.x方式又有点点变化。

要把字符串转换为对象, 无论是XML, JSON, spring mvc ctrl参数自动生成, spring自身的SPEL, 甚至是java自带的反串行化, 实际上一直一起来都或多或少有些安全漏洞。 我们能做的就是及时升级消除隐患。