Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

15 | 序列化:一来一回你还是原来的你吗?

枚举作为API接口参数或返回值的两个大坑
反序列化时要小心类的构造方法
注意Jackson JSON反序列化对额外字段的处理
序列化和反序列化需要确保算法一致
序列化和反序列化常见坑点

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。今天,我来和你聊聊序列化相关的坑和最佳实践。
序列化是把对象转换为字节流的过程,以方便传输或存储。反序列化,则是反过来把字节流转换为对象的过程。在介绍文件 IO的时候,我提到字符编码是把字符转换为二进制的过程,至于怎么转换需要由字符集制定规则。同样地,对象的序列化和反序列化,也需要由序列化算法制定规则。
关于序列化算法,几年前常用的有 JDK(Java)序列化、XML 序列化等,但前者不能跨语言,后者性能较差(时间空间开销大);现在 RESTful 应用最常用的是 JSON 序列化,追求性能的 RPC 框架(比如 gRPC)使用 protobuf 序列化,这 2 种方法都是跨语言的,而且性能不错,应用广泛。
在架构设计阶段,我们可能会重点关注算法选型,在性能、易用性和跨平台性等中权衡,不过这里的坑比较少。通常情况下,序列化问题常见的坑会集中在业务场景中,比如 Redis、参数和响应序列化反序列化。
今天,我们就一起聊聊开发中序列化常见的一些坑吧。

序列化和反序列化需要确保算法一致

业务代码中涉及序列化时,很重要的一点是要确保序列化和反序列化的算法一致性。有一次我要排查缓存命中率问题,需要运维同学帮忙拉取 Redis 中的 Key,结果他反馈 Redis 中存的都是乱码,怀疑 Redis 被攻击了。其实呢,这个问题就是序列化算法导致的,我们来看下吧。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了序列化和反序列化在软件开发中的重要性和应用。首先介绍了序列化算法的选择和权衡性能、易用性和跨平台性等因素的重要性。其次重点讨论了在实际开发中常见的序列化问题,如Redis、参数和响应序列化反序列化,并提出了相应的解决方案。此外,还介绍了在使用RedisTemplate和StringRedisTemplate时的注意事项,以及如何自定义RedisTemplate的Key和Value的序列化方式。最后,通过具体案例分析了Jackson JSON反序列化对额外字段的处理和反序列化时要小心类的构造方法的问题,并提供了相应的解决方案。整体而言,本文内容涵盖了序列化和反序列化的选择、常见问题以及在Redis中的应用,为读者在实际开发中避免常见的序列化坑提供了指导。文章深入浅出,适合开发人员快速了解序列化和反序列化的相关知识。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(19)

  • 最新
  • 精选
  • 梦倚栏杆
    老师,现在像fastJson, jackson 一般使用序列化和反序列化不都是属性类型兼容就能来回序列化吗?java序列化的时候存储序列化id记录版本号的意义是什么。 java序列化一开始存在的意义是什么?为什么要那样处理呢?如果按照现在fastJson 和jackson等的处理方式,toString 不也是一种序列化方式吗?反序列化时按照一种规则解析回去不就行了

    作者回复: 1、有关serialVersionUID的意义: The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will result in an InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field named serialVersionUID that must be static, final, and of type long: ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L; If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class serialVersionUID fields are not useful as inherited members. 2、toString也可以认为是一种文本序列化,序列化当然还可以按照自己的方式来做,只要是一致的方式实现对象到字节的转换。 3、 java序列化一开始存在的意义是什么?在有xml、json、protobuf等之前,jdk总需要有序列化来实现对象的文件存储、跨服务传输吧,当时确实互联网也没这么发达没考虑到异构体系的交互问题,我们不能以现在的眼光来看当时的技术为什么考虑这么不全面这么鸡肋

    2020-04-14
    25
  • Darren
    试着回答下今天的问题: 1、Long序列化的时候,Redis会认为是int,因此是获取不到的Long数据的,需要处理; 2、Jackson2ObjectMapperBuilder的采用了构建者模式创建对象;调用的是build()方法 public <T extends ObjectMapper> T build() { ObjectMapper mapper; if (this.createXmlMapper) { mapper = (this.defaultUseWrapper != null ? new XmlObjectMapperInitializer().create(this.defaultUseWrapper) : new XmlObjectMapperInitializer().create()); } else { mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper()); } configure(mapper); return (T) mapper; } 然后configure里面出了了甚多事情,比如:日式、Local、时间等的格式化器以及自定义属性命名策略等,具体详见https://github.com/y645194203/geektime-java-100/blob/master/ConfigInfo.java

    作者回复: 比较坑的是,在Integer区间内返回的是Integer,超过这个区间返回Long @GetMapping("wrong2") public void wrong2() { String key = "testCounter"; countRedisTemplate.opsForValue().set(key, 1L); log.info("{} {}", countRedisTemplate.opsForValue().get(key), countRedisTemplate.opsForValue().get(key) instanceof Long); Long l1 = getLongFromRedis(key); countRedisTemplate.opsForValue().set(key, Integer.MAX_VALUE + 1L); log.info("{} {}", countRedisTemplate.opsForValue().get(key), countRedisTemplate.opsForValue().get(key) instanceof Long); Long l2 = getLongFromRedis(key); log.info("{} {} {}", l1, l2); } private Long getLongFromRedis(String key) { Object o = countRedisTemplate.opsForValue().get(key); if (o instanceof Integer) { return ((Integer) o).longValue(); } if (o instanceof Long) { return (Long) o; } return null; } 输出: 1 false 2147483648 true 1 2147483648

    2020-04-14
    3
    22
  • 左琪
    老师,我之前遇到一个,我用redis存入一个Map<Long,object>,取出时发现却是Map<int,object>,然后响应给前端springmvc就报类型转换异常了,我redis的value也是用的Jackson序列化,自定义了objectmapper,正常对象都能序列化,反序列化,就是Long不行,我想知道该如何修正呢

    作者回复: 这个long和int的问题应该就是我思考题的问题,你可以看看其他网友的回复以及我的回复如何解决。 我不知道你这里的用redis存入Map<Long,object>是不是指key是Long,value是Object,如果是的话,把数字作为Key不是一个好的实践,Redis的Key需要是字符串,并且区分命名空间,比如应用_领域_标识(或是数据库_表_PK),e.g.commonmistakes_redisexample_user123

    2020-04-16
    10
  • 👽
    之前其实一直还是比较喜欢枚举的,一直只是觉得枚举这个是个好的功能,只是我不会用。 现在来看,看来枚举在使用上确实时需要谨慎。 个人理解,枚举的本质,其实就是一个Map<Object,Object>,但是扩展性更强一些。其本身的存在,类似于一个 不可变的常量Map,本身的存在与意义,个人感觉,与数据字典也很像。存索引值(key),但对应一个具体对象或数值(value)。经过这一讲,之后的业务,我个人可能也会使用数据字典,而慎用枚举了。

    作者回复: 内部没关系,也推荐使用枚举,对外是要慎用

    2020-04-14
    7
  • 扎紧绷带
    有个同学说不用 int 来枚举,而选择语义性的字符串。我也觉得语义明确的字符串更好一些,但很多人认为数字占空间小,应该用int。老师怎么看,你们是怎么用的呢?

    作者回复: 字符串略好点,空间方面其实不差这些

    2020-07-04
    6
  • pedro
    关于枚举,无论是在 dto 还是数据库存储,我们都已经不用 int 来枚举了,而选择语义性的字符串,这在 debug 和维护上十分方便,也有利于迁移,int 枚举太难看了,每次调试,眼睛都花了😭

    作者回复: 😀

    2020-04-14
    2
    5
  • Michael
    我们项目中遇到的坑是:key是字符串,value是一个自定义对象,我们环境分为inner和prd,inner验证过了才会发生产,但是inner和prd是同一个Redis缓存和DB,value值对应的对象中加了字段,生产和inner同时作用,prd缓存失效了,正好把inner的给存进去了,结果导致生产接口从缓存取数据的时候出现反序列化报错问题,影响了生产。 后面采取方法是在缓存key加上环境前缀来避免这个问题。

    作者回复: 对内环境和生产环境的问题也是比较典型的。对内虽然和生产公用数据库和中间件,但是毕竟还是用于测试验证的。 之前遇到的一个坑是类似的,对内环境和生产公用CDN,对内验证的时候传了一张测试图片上去,然后CDN节点就有了这个文件,到了生产上虽然又传了正式的图片(文件名没有变),但是CDN节点上的测试图片依旧缓存着,部分地区用户还是看到了测试图片。。。

    2020-04-19
    2
    2
  • 💢 星星💢
    老师,我最近遇到一个坑,也是Jackson 序列化反序列的。一开始在xml中定义只定义mappingJacksonHttpMessageConverter,然后在DTO中某日期字段中加上@JsonDeserialize(using = DateJsonDeserialize.class)能实现日期转为功能。但是前台页面中显示的日期全都为时间戳。 于是在定义了ObjectMapper 里面重写了日期格式化的序列化方法,然后原先的进行接口Json格式反序列,@JsonDeserialize这个功能不能用了,找了好久都没有解决办法。老师这个坑,该如何解决?

    作者回复: 最好帖下代码以及测试用例,否则很难看懂

    2020-07-07
    1
  • 小杰
    log.info("longRedisTemplate get {}", (Long)longRedisTemplate.opsForValue().get("longRedisTemplate")); java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long 强转异常,也就是说我们获取到这样的值还要自己从Integer转成Long是吗老师?

    作者回复: 是

    2020-04-14
    1
  • 捞鱼的搬砖奇
    ObjectMapper 的 activateDefaultTyping 方法 在2.10版本才提供,那之前的版本有替代方案吗。

    作者回复: 之前是enableDefaultTyping吧

    2020-08-21
收起评论
显示
设置
留言
19
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部