Java序列化陷阱揭秘:这5个错误80%的开发者都犯过

01 引言

Java序列化是将Java对象转换为字节流的过程,以便存储或传输;反序列化则是将字节流恢复为Java对象的过程。这一机制使得对象能够超越JVM的生命周期而存在,也便于在网络间传输。

最近总是遇到序列化与反序列化的问题,不是自己遇到就是帮助同事排查类似的问题,本节总结5个常见的错误,看看你有没有踩坑。

  • getter方法异常
  • 序列化与反序列化不一致
  • JavaBean变更
  • 序列化工具混用
  • Serializable

02 getter方法异常

这个错误是一个同事遇到的,给我说他就在getter方法里面改了点东西,结果方法调用序列化时就异常了。而且getter方法本身没有属性,怎么会影响序列化结果呢?

从截图来看,是fastjson 1.2.54在序列化对象的时候报的错。

2.1 场景复现

定义一个实体类,里面定义了一个getter,直接抛出异常。

java 复制代码
@Data
@AllArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;
    private String name;
    private int age;

    public String getTest() {
        throw new RuntimeException("模拟异常。。。。");
    }
}

2.2 测试

测试代码:

java 复制代码
@Test
void test01() {
    User user = new User(1, "鸣人", 18);
    System.out.println(JSON.toJSONString(user));
}

结果:

可以看到:

getTest并没有对应的属性,但是序列化的时候会调用,触发异常。查看源码发现框架会将所有的getter方法,也会作为属性。

所以,序列化本身看似序列化对象的属性,和方法看似没有关系,但是getter方法也会作为属性,获取值时会调用getter方法,从而触发异常。

03 序列化与反序列化不一致

这个是线上经常遇到的,我们先来看一段代码:

java 复制代码
// 存入缓存
redisService.setExpireDate(key, val, date);

// 从缓存中获取值
String val = redisService.get(key)

结果代码发布之后,出现了反序列化异常。看了封装的源码之后,才恍然大悟。

3.1 序列化

代码中采用了自定义的对象流的序列化方式。

3.2 反序列化

Redis的反序列化直接返回了,我们知道不配置的情况下,序列化和反序列化采用的事JDK自带的序列化方式(JdkSerializationRedisSerializer)。

3.3 结论

显然序列化和反序列化的方式不一样,导致反序列化异常。这个问题主要就是因为封装的工具类定义混乱,导致使用者产生歧义,从而容易踩坑。

04 JavaBean变更

在日常开发中,JavaBean的字段会根据需求变更。假设v1版本的User已经序列化放在缓存里了。

json 复制代码
{
    "age": 18,
    "id": 1,
    "name": "鸣人",
    "job": "火影"
}

但是由于需求变更job不在需要了,开发人员在v2版本中删除了这个字段。但是缓存中的数据忘记更新了,jackson默认反序列化的时候,就会出现异常。

4.1 模拟案例

java 复制代码
 @Test
void test04() throws JsonProcessingException {
    String jsonStr = "{\"age\":18,\"id\":1,\"name\":\"鸣人\",\"job\":\"火影\"}";

    ObjectMapper objectMapper = new ObjectMapper();
    System.out.println(objectMapper.readValue(jsonStr, User.class));
}

结果:

4.2 解决方案

清除缓存的方法我们就不讲了,我们就单纯从技术上看怎么解决。

增加反序列化的配置:

java 复制代码
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES默认为true,字段不匹配时就会抛出异常。设置成false,就可以解决。

使用注解:

使用注解忽略不存在的字段:

@JsonIgnoreProperties(ignoreUnknown = true)

最终都可以实现反序列化:

05 序列化工具混用

这个不难理解,其实就是序列化与反序列化工具不一致的问题。为什么要单独讲呢?

因为现在工具类泛滥,尤其json的处理。标准的json处理几乎都没有问题,但是遇到特别的数据,就可以出现问题。不仅仅工具类,相同工具类的不同版本也有了能出现问题,这种问题最难排查,因为问题是偶发的,不可固定重现。

json序列化框架有好多,如fastjsonjackjsongson等。

日期异常案例:

java 复制代码
@Test
void test05()  {
    User user = new User(1, "鸣人", 18);
    user.setBirthday(new Date());
    String jsonString = JSON.toJSONString(user);
    System.out.println(jsonString);

    Gson gson = new Gson();
    System.out.println(gson.fromJson(jsonString, User.class));
}

我们可以看到fastsjsongson对日期的处理不一致,导致反序列化异常。

06 Serializable

实现Serializable接口,可以保证传输类在序列化后传递,Serializable之前专门讲过一期,这里不在赘述。

对象在使用java.io.ObjectOutputStream序列化的时候必须实现Serializable接口,否则就会序列化失败。

6.1 案例

这里的User没有实现Serializable接口。

java 复制代码
@Test
void test06() {
    User user = new User(1, "鸣人", 18);
    System.out.println(user);
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(outputStream)) {
        oos.writeObject(user); // 成功
    } catch (Exception e) {
        e.printStackTrace();
    }
}

6.2 效果

直接报出异常:java.io.NotSerializableException

实现Serializable就恢复正常了。

07 小结

Java序列化与反序列化是一把威力巨大但亦需谨慎使用的"双刃剑"。它赋予了对象持久化与网络传输的能力,但同时也埋下了性能陷阱与安全漏洞的隐患。

在使用过程中要注意工具的统一,网络传输实现序列化接口等,消灭潜在的危机。

相关推荐
齐 飞2 小时前
Spring Cloud Alibaba快速入门03-OpenFeign进阶用法
spring boot·后端·spring cloud
程序员清风2 小时前
快手二面:Redisson公平锁用用过吗?他的实现原理是什么样子的?
java·后端·面试
Seven972 小时前
Redis容量评估模型
java·redis
app出海创收老李2 小时前
海外独立创收日记(3)-第一个内购
程序员
深圳蔓延科技2 小时前
Kafka + Spring Boot 终极整合指南
后端·kafka
风象南2 小时前
SpringBoot 方法级耗时监控器
后端
€8113 小时前
Java入门级教程16——JUC的安全并发包机制
java·开发语言·juc的安全并发包机制·栅栏机制·闭锁机制·信号量机制·无锁机制
杨杨杨大侠3 小时前
Atlas Mapper 教程系列 (2/10):环境搭建与项目初始化
java·开源·github