采坑系列--fastjson

  接口协议格式,相信很多人都在用json。而在国内,大多人都用过开源工具fastjson。多对象格式的支持,简单、快速的序列化和反序列化操作,深得广泛应用。不过咖啡最近在使用过程中遇到了一些问题,而且官方也并未完全处理。下面详述一下采坑的过程。

应用背景

  众所周知,Json是一种轻量级的数据交换格式,采用一种“键:值”对的文本格式来存储和表示数据,在系统交换数据过程中常常被使用,是一种理想的数据交换语言。所以json就被广泛的应用于接口协议文本格式。
  阿里的fastjson工具,开源并且使用方便。
  我们常用到的方法:

  • 序列化:JSONObject.toJSONString(obj)/JSONObject.toJSON(obj) 等;
  • 反序列化:JSONObject.parse(str)/JSONObject.parseObject(str)/JSONObject.parseObject(str, Obj.class) 等;
    (obj代表传入的实例对象类型,str为json格式的字符串)
实例分析

  一般我们常用JSONObject.toJSONString(obj)直接格式化成json格式字符串,这种序列化能满足通用的场景需求,下游进行反序列化的时候,也能正常解析。 但这个方法有一个问题,会直接剔除掉值为null的字段,当下游去解析对象,需要表头的时候,被剔除掉的字段就不会被解析出来。如:

public static void main(String[] args) {
    //定义map对象
	Map<String, Object> param = new HashMap<>();
	param.put("id", 123456);
	param.put("dcCode", "X00X");
	param.put("dcName", null);
	String jsonStr1 = JSONObject.toJSONString(param);
    System.out.println(jsonStr1);
}
//原对象内容格式化后输出应为:
{"id":123456789,"dcCode":"x00X","dcName":null}
//实际输出:
{"id":123456789,"dcCode":"x00X"}

  查阅官方释义后,发现 JSONObject.toJSONString(obj) 这个方法本身并不存在问题,之所以这么做是为了增加格式化的效率,为高性能忽略掉null值的字段格式化是值得的,大部分业务场景下我们也不需要去解析空值字段。但就是某些特定场景下,我们需要再去解析全部表头的时候,是需要准确数据格式的。为此官方也给出了相应解决方案,使用方法:

JSONObject.toJSONString(Object object, SerializerFeature… features)

可以自定义输出配置。 其中com.alibaba.fastjson.serializer.SerializerFeature参数也是fastjson自定义的一个枚举类,部分枚举类型的参考含义如下:

名称含义
QuoteFieldNames输出key时是否使用双引号,默认为true
UseSingleQuotes使用单引号而不是双引号,默认为false
WriteMapNullValue是否输出值为null的字段,默认为false
WriteEnumUsingToStringEnum输出name()或者original,默认为false
UseISO8601DateFormatDate使用ISO8601格式输出,默认为false
WriteNullListAsEmptyList字段如果为null,输出为[],而非null
WriteNullStringAsEmpty字符类型字段如果为null,输出为“”,而非null
WriteNullNumberAsZero数值字段如果为null,输出为0,而非null
............

可以看到 WriteMapNullValue,正是我们需要的配置,我们可以把方法改为:
JSONObject.toJSONString(Object object, SerializerFeature.WriteMapNullValue)

//输出变为:
{"id":123456789,"dcCode":"x00X","dcName":null}

  那么现在是不是已经完全达到我们想要的效果了呢?我们在配置参数中又发现一个参数: WriteNullStringAsEmpty(字符类型字段如果为null,输出为“”,而非null) ,比如:当我们需要将展示的数据展示在前台界面,而不需要在前端做任何转换的时候,我们就有必要将控制自动转换为空字符串,这样我们的通用性就更高了。当然我们可以后台手动处理,但如果要一个个地去判空设置,我相信没有多少程序员愿意这么去干。既然官方支持传入动态长度的配置数组,我们是不是将方法改写成为:
JSONObject.toJSONString(Object object, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty),可惜事与愿违,输入结果依然为:

{"id":123456789,"dcCode":"x00X","dcName":null}

  开始觉得一定是自己用法不对,改为JSONObject.toJSONString(Object object, new SerializerFeature[]{SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty}),结果依然不变。
那只能祭出终极方案:查阅源码。DEBUG层层深入,终于在com.alibaba.fastjson.serializer.MapSerializer类的第211行发现了苗头

@SuppressWarnings({ "rawtypes"})
public void write(JSONSerializer serializer
            , Object object
            , Object fieldName
            , Type fieldType
            , int features //
            , boolean unwrapped) throws IOException{
    ...
210 if (value == null) {
211     if (!out.isEnabled(SerializerFeature.WriteMapNullValue)) {
212         continue;
213     }
214 }
    ...
}

  也就是当你序列化配置枚举类型为WriteMapNullValue时才生效(out.isEnabled怎么判断的,断点进去即可明白),否则跳过枚举处理。那其余的对null值做处理的序列化配置是用来做装饰的吗?咖啡当前所使用的fastjson版本是fastjson最新版1.2.58,由于不甘心,继续翻阅以前版本的源代码,终于发现了差异,在1.2.29版本同样的方法下发现如下代码(注意第200行):

199 if (value == null) {
200    if (!out.isEnabled(SerializerFeature.WRITE_MAP_NULL_FEATURES)) {
201        continue;
202    }
203 }

WRITE_MAP_NULL_FEATURES的类型如下:

public static final int WRITE_MAP_NULL_FEATURES
    = WriteMapNullValue.getMask()
    | WriteNullBooleanAsFalse.getMask()
    | WriteNullListAsEmpty.getMask()
    | WriteNullNumberAsZero.getMask()
    | WriteNullStringAsEmpty.getMask()
    ;

是不是看明白了,只有在1.2.29版本及以前版本,才支持这几种空值的序列化处理。
奇葩的事还在后面,即使是处理了这几种类型,最终依然只走到一个方法:

if (value == null) {
    out.writeNull();
    continue;
}
public void writeNull() {
    write("null");
}

也就是即使我通过了处理,最终我还是任性地给你输出一个null。在SerializeWriter类下的writeNull的多态方法中,发现了处理痕迹,却没被MapSerializer调用!

public void writeNull(int beanFeatures , int feature) {
    if ((beanFeatures & feature) == 0 //
        && (this.features & feature) == 0) {
        writeNull();
        return;
    }
    
    if (feature == SerializerFeature.WriteNullListAsEmpty.mask) {
        write("[]");
    } else if (feature == SerializerFeature.WriteNullStringAsEmpty.mask) {
        writeString("");
    } else if (feature == SerializerFeature.WriteNullBooleanAsFalse.mask) {
        write("false");
    } else if (feature == SerializerFeature.WriteNullNumberAsZero.mask) {
        write('0');
    } else {
        writeNull();
    }
}

最终发现是官方不支持,不知是有意而为之,还是久久得不到修复-_-||
那么,最终我们想要得到我们想要的效果,绝不能止步于此。好在fastjson还提供了一个自定义序列化接口ValueFilter,那就简单了,自定义序列化实现接口:

private static ValueFilter filter = new ValueFilter(){
    @Override
    public Object process(Object obj, String s, Object v) {
	    if(v==null)
	        return "";
	    return v;
    }
};

public static void main(String[] args) {
	Map<String, Object> param = new HashMap<>();
	param.put("Id", 123456);
	param.put("dcCode", "X00X");
	param.put("dcName", null);
	String jsonStr = JSONObject.toJSONString(param, filter);
	System.out.println(jsonStr);
}

执行输出:

{"dcName":"","Id":123456,"dcCode":"X00X"}

至此,fastjson踩坑以及填坑完毕。即使是官方开源框架,我们还是会踩到大坑,只要多思考几步,究其深处,还是会找到填坑的办法的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值