FastJson序列化属性名格式诡异现象

      本人能力有限,不一定完全正确。在里写是为了自己以后方便查阅.对于其他人可以起到抛砖引玉的作用。

现象:

     FastJson序列化属性名的格式在同一应用有的序列化后属性名是驼峰格式userId:1000,有的序列化后是Snake格式user_id:1000。

应用:

     FastJson在1.2.15版本之后支持配置的PropertyNamingStrategy四种策略: CamelCase、PascalCase、SnakeCase和KebabCase,默认是驼峰(CamelCase)格式。

       我们的项目是springboot+spring mvc,配置了fastjson作为json报文的HttpMessageConverter:

FastJson的SerializeConfig.getGlobalInstance().propertyNamingStrategy是全局配置。

应用里有一个请求是异步的,调用方请求完后,请求的数据会调用一个工具包common-util里的异步处理把请求数据保存到Redis里。这个请求就立即返回本次调用。common-util里的异步组件会起一个任务实时处理redis了的请求。相当于一个MQ,只不过MQ的消费者和生产者在一个应用里。消费者需要继承common-util里一个抽象的ProduceConsumer,并实现具体的处理逻辑。在消费者里处理完后会调用JSON.toJSONString()把处理结果序列化,然后发送到真正的MQ里,供调用者消费。

有一次升级了common-util已经其他包后未修改任何地方,结果发布到线上后,发生了原本是json的属性是snake格式变成了camel格式。这就导致了消费者如果不是使用的fastjson反序列化的话改变的属性就无法解析了,导致业务方的成功率直线下降。第一感觉应该是FastJson的包冲突导致,经过专家和我的分析发现确实FastJson的版本有好几个包都是其他jar带入的。后来把所有其他的版本都exclude掉再发线上。果然好了。

经过一段时间后有新需求。就开发了新需求,紧接着就是上线。因为上次出现过fastjson的问题,所以这次就特别关注下。结果发完第一台后就发现MQ的消息上次的问题又出现了。于是赶紧看是否又有包冲突了,果然发现这次导入新的依赖包又带入一个。然后把该包exclude掉,再发还是没有解决。这就令人非常的困惑了。于是让测试再次发布这台机器,经过2次重复发布后,结果MQ的消息又神奇的变回了Snake的格式。于是后面的几台机器都这样发布,都是发2次后变回需要的带下划线的格式。

我的天啦,好神奇啊。明明FastJson的SerializeConfig.getGlobalInstance().propertyNamingStrategy是全局配置,且JSON.toJSONString()序列化的时候是用的GlobalInstance。部分代码如下:

public class SerializeConfig {

    public final static SerializeConfig                   globalInstance  = new SerializeConfig();
public static SerializeConfig getGlobalInstance() {
   return globalInstance;
}

}


  JSON.toJSONString()的部分代码:
public static String toJSONString(Object object, SerializeFilter filter, SerializerFeature... features) {
    return toJSONString(object, SerializeConfig.globalInstance, new SerializeFilter[] {filter}, null, DEFAULT_GENERATE_FEATURE, features);
}

所以他们序列化使用的配置应该就是Snake的。现在又没有包的冲突可以解释了。还有什么原因呢。唯一的办法只有看源码了。

看源码是一个比较头疼的事,必须通过源码看懂作者的思想。经过呕吐后发现FastJson在真正序列化时跟到如下代码

Class<?> clazz = object.getClass();(需要序列化的对象)
ObjectSerializer writer = getObjectWriter(clazz);
public ObjectSerializer getObjectWriter(Class<?> clazz) {
    return config.getObjectWriter(clazz);
}

SerializeConfig.java

 

private final IdentityHashMap<Type, ObjectSerializer> serializers;
private ObjectSerializer getObjectWriter(Class<?> clazz, boolean create) {
       ObjectSerializer writer = serializers.get(clazz);
       if (writer == null)
       ----------------------------------------中间很多代码省略---------------------------------------------------
       if (create) {
         put(clazz, createJavaBeanSerializer(clazz));
    }

从HashMap里根据需要序列化的class查询对应的ObjectSerializer来序列化。如果没有查到就创建一个该对象的ASM的ObjectSerializer并保持到HashMap,具体没有细看。应该是根据配置生成了对应对象序列化的格式,以后每次序列化就使用这个保持的对象进行序列化。

       这样只知道FastJson在第一次toJSONString的时候会生成对应实例的序列化ObjectSerializer保存到缓存以提高序列化的速度。还是没法解释问题。

    正常的情况就是系统发布完成FastJson的HttpMessageConverter被spring实例化且FastJson的GlobalInstance().propertyNamingStrategy被设置成Snake模式,同时继承的ProduceConsumer消费者也是被spring管理的,在应用启动完成后都会实例化。然后业务请求过来,然后消费者消费,然后JSON序列化且使用GlobalInstance().propertyNamingStrategy的配置。

            然后再去看ProduceConsumer里是如何启动消费者的。

     ProduceConsumer的构造函数部分代码如下,在实例化消费者的时候就其他线程池任务去Redis里消费

public ProduceConsumer(
    Class<T> clazz, int queueSize, int retryTimes, int processTimeoutMillis, boolean isAsync) {
        ThreadPool.run(new ConsumeQueueTask());
         ThreadPool.run(new ConsumeFailedQueueTask());
}

意思就是说在spring容器启动时实例化这个消费者实例的时候就会调用该构造函数进行实例化,就启动了线程去消费。

那么。。。。。。。。。。。。。。。。。。有没有可能是spring实例化HttpMessageConverter和消费者的顺序不一样导致消费者在HttpMessageConverter实例化前实例化消费者,此时启动了线程消费且Redis里有Messae。而此时配置在HttpMessageConverter的FastJson的propertyNamingStrategy还没有赋值,而后消费者在序列化的时候就使用了默认的驼峰格式且保存了驼峰格式的ObjectSerializer,以后无论什么时候都是驼峰格式的了,不会再参考propertyNamingStrategy。因为第一次序列化的时候ObjectSerializer就缓存了。

        于是加入日志打印发现ProduceConsumer的消费者确实是在HttpMessageConverter实例化前被spring容器实例化。

        终于可以松口气了,诡异的现象应该就是这个原因引起的了。

        怎么解决呢?

        1.每次序列化是new SerializeConfig-------------------性能肯定不行

        2.在要序列化的实体上加入@JSONFiled指定序列化的属性名称-----------麻烦

        3.在spring容器实例化ProduceConsumer前配置propertyNamingStrategy。可以实现一个BeanFactoryPostProcessor在里面配置 SerializeConfig.getGlobalInstance().propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;

经测试方案3是可行的。

 

 

        

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值