当前位置: 首页 > article >正文

FastJson序列化驼峰-下划线转换问题踩坑记录

背景

问题描述

在MySQL数据表中,存在一个JSON结构的扩展字段,通过updateById进行更新写入操作。更新写入的同一个字段名出现了混合使用了驼峰命名和下划线命名两种格式。
ps: FastJson版本是1.2.83

问题影响

数仓同学离线统计数据时发现字段名有两种定义,产生了迷惑,不清楚如何统计;对线上实时链路还无任何影响。

原因定位

  • 首先例行公事,先检查多机房部署的代码版本是否一致,类似数据是否有持续写入
    • 检查过后发现,多区代码部署版本完全一致;
    • 且数据字段命名驼峰&下划线同时存在的数据持续有写入;
  • 检查下代码中是否有不同的写入位置,且写入的字段定义规则不一致
    • 发现这个字段只有一处更新写入,且通过业务日志看这个字段没有任何问题;
  • 尝试测试环境复现
    • 首先检查了下测试环境是否存在类似数据,发现并没有;
    • 其次在测试环境尝试构造请求写入,并未复现;
  • 到了常规猜测环节
    • 首相想到的是否有配置了全局的fastjson序列化驼峰下划线转换配置
      • 发现确实是有配置的,全局配置成了驼峰格式转换为下划线;配置如下:
@Configuration
public class FastJsonConfig {

    @Bean
    public SerializeConfig serializeConfig(){
        SerializeConfig serializeConfig = SerializeConfig.getGlobalInstance();
        serializeConfig.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
        return serializeConfig;
    }
}
  • 那么新的问题来了,配置了全局转换为下划线的情况呢?
    首先在mybatis Xxx.xml 中的update语句里有
<if test="extraInfo != null">
        extra_info = #{extraInfo, typeHandler=com.xxx.config.typehandler.JsonTypeHandler},
</if>

result里面配置有如下转换:

<result column="extra_info" jdbcType="VARCHAR"
            javaType="com.xxx.dao.model.SoundtrackDO$SoundtrackExtraInfo"
            typeHandler="com.xxx.config.typehandler.JsonTypeHandler"
            property="extraInfo"/>

JsonTypeHandler 的代码如下:

public class JsonTypeHandler<T> extends BaseTypeHandler<T> {

    private Class<T> type;

    public JsonTypeHandler(Class<T> type) {
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return parseObject(rs.getString(columnName));
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return parseObject(rs.getString(columnIndex));
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return parseObject(cs.getString(columnIndex));
    }

    private T parseObject(String result) {
        return JSON.parseObject(result, type);
    }
}

从配置代码上没有看出任何毛病,自测也是符合预期,一切都像是那么合理,为什么线上环境就出现了意外场景呢?依然很疑惑。
到了这里头上顶了三个问号,难道说这跟bean的初始化顺序有关?因为这个场景我们是消费rocketMq消息后调用mysql的更新语句,也就会执行到JsonTypeHandler这里的代码;而这里如果是消费者bean先初始化完成,代码开始消费,而此时序列化全局配置bean还没初始化完成是不是就用了默认的序列化规则呢,看起来很合理的猜测,所以我们开始了验证:

首先验证默认情况:
在这里插入图片描述
在这里插入图片描述
从图上看默认是驼峰,很符合预期;
那我们就尝试复现下先序列化,在设置全局参数的场景:
在这里插入图片描述
在这里插入图片描述
我们是先开启线程1序列化输出ss1,主线程睡眠两秒后,执行全局赋值驼峰下划线转换策略,再开启线程2,序列化输出ss2;从执行结果中可以看出,问题已经出现了,在我们设置驼峰转下划线格式后,ss2输出的依然是驼峰格式。
至此我们基本定位问题为:序列化策略propertyNamingStrategy参数赋值与序列化执行先后关系导致;从业务代码角度来说的话,就是RocketMq consumer bean初始化(因为初始化后,有消息就会立即消费,并不会等所有不强相关bean都初始化完成) 与 serializeConfig bean初始化先后顺序导致;测试环境因为消息太少所以没有命中这个问题。

接下来我们又提出个疑问,1.我们可以看到JSON.toJSONString方法是每次执行都会去获取SerializeConfig.globalInstance,那么如果每次都获取是不是服务启动完成后,就恢复了呢,但从数据库的数据上看并不是;
通过debug我们发现,fastjson 对一个对象首次序列化之后会将其存储再一个容器内com.alibaba.fastjson.serializer.SerializeConfig#serializers;后续在对这个对象序列化时是通过获取容器中存储的序列化规则进行处理的,并不是重新获取一遍配置,这也就是为啥服务启动完成后没有恢复的原因;也是fastjson的一个提升性能的设计。
对于为什么在线链路没有出现问题,这就是fastjson的一个机制了,反序列化时会先匹配字段名完全相同的字段值,没有的情况下还会匹配对应的驼峰或者下划线格式的字段名进行赋值,所以从接口返回上看不出问题。
好了,问题已经定位完成,后续就是如何解决了;

解决方案

1.提高serializeConfig bean的加载时机

例如在serializeConfig 配置处添加 @Order(1) 注解

但这个方式后续如果有人调整调整优先级可能会重新出现这个问题;
2.添加Consumer bean和 serializeConfig bean的依赖关系

例如在Consumer bean上配置@DependsOn("serializeConfig")

这样影响范围最小,切不会轻易受到其他调整的影响
3.抛弃serializeConfig全局配置,在使用场景进行单独处理,这个就是乱说了,这种场景完全不敢尝试,虽说看起来是短痛;不在有类似的问题出现,但过于痛,可能会踩大坑,不建议。

最终选择方案2

总结一下

1.这种全局配置要谨慎使用,且这种加载优先级设置还不是最高的,且不好被开发人员发现的配置话尽量少用吧,避免坑自己更避免坑别人
2.对于老服务,还是需要多观察下配置


http://www.kler.cn/a/283814.html

相关文章:

  • 带你掌握springboot集成SpringSecurity安全框架
  • 计算机毕业设计必看必学35755flask旅游景区热度可视化平台原创定制程序,java、PHP、python、小程序、文案全套、毕设成品等
  • 中文书籍对《人月神话》的引用(161-210本):微软的秘密
  • 矩阵的各种计算:乘法、逆矩阵、转置、行列式等——基于Excel实现
  • 游戏引擎学习第七天
  • 论文分享:DiskANN查询算法
  • 基于Spring Boot的文字识别系统
  • 逆波兰表达式求值
  • 【面试经验】华为产品行销经理面经
  • 数据赋能(187)——开发:数据产品——概述、关注焦点
  • 超详细Git的基本命令使用(三)
  • C++入门基础知识43——【关于C++循环】
  • Golang | Leetcode Golang题解之第384题打乱数组
  • RclimDex使用方法
  • 晟鑫商会与家盛资本携手合作,共创金融科技新篇章
  • uniapp踩坑实战之引用‘uview-ui‘
  • MySQL数据库设计基础:从零开始构建你的第一个数据库
  • 算法工程师第五十一天(dijkstra(堆优化版)精讲 Bellman_ford 算法精讲)
  • Python——模块和包
  • Tengine框架之配置表的Luban转换与加载
  • 数据分析学习之numpy
  • static关键字与单例模式
  • el-table自定义合并表格
  • 为什么 CNC 加工会产生毛刺?
  • 如何在 Vue 中创建一个带有表格和表单的弹窗
  • 数据结构之十字链表