JDK 25 重大兼容性 Bug

Java 25 是今年 9 月 16 号发布的,距今不足 2 月。已经发现了好几个 bug 了。这些 bug 当中,有几个已经修复了。于是,官方陆续的发布了几个对应 JDK 的修复版本,其中针对 Java 25,发布的是 JDK 25.0.1。在这个版本中,有一个影响 JDK 25 重大兼容性陷阱,那就是 java.time 序列化问题。本文将深度解析一下。

前言

JDK 25 带来了很多激动人心的新特性,我也写了好几篇陆续关于 JDK 25 新特性的文章。但在升级过程中,一些开发者们发现在使用过程中可能会遇到一个隐藏的"地雷",即 java.time 包中几个核心类的序列化兼容性问题。这个问题可能导致你的应用在升级后无法正常反序列化数据,甚至抛出 InvalidClassException。这个由 JDK-8367031 记录的 bug 终于被 PR 了。

受影响的核心类

收到影响的几个类,如下所示。

  • java.time.LocalDate
  • java.time.YearMonth
  • java.time.MonthDay
  • java.time.chrono.HijrahDate

问题的本质

JDK 25 与之前版本之间存在序列化不兼容性,但这里有一个重要的细节:只有 Class 对象的序列化受影响,实例对象的序列化是正常的

也就是说,升级 JDK 25 之前是好的,升级之后可能就会出现InvalidClassException问题。

技术根因分析

根据官方修复的https://bugs.openjdk.org/browse/JDK-8367031介绍显示,这个兼容性问题的根源可以追溯到 JDK-8334742,该变更是为了支持 Valhalla 项目中的值类(Value Classes)特性。Valhalla 项目旨在为 Java 引入值对象,结合面向对象编程的抽象性和简单原语的性能特征。

字段类型变更

官方为了实现更紧凑的内存布局,JDK 团队将以下字段的类型从 short 改为 byte

复制代码
// 变更前的字段类型(JDK 24及之前)
privatefinalshort year;
privatefinalshort month;
privatefinalshort day;

// 变更后的字段类型(JDK 25)
privatefinalint year;    // year保持int类型
privatefinalbyte month;  // month从short改为byte
privatefinalbyte day;    // day从short改为byte

JDK17

这个紧凑的内存布局,短了一字节,也短了命,让 JDK 25 的"字节"级事故坑到了不少老外尝鲜的网友。

为什么实例序列化不受影响?

不得不说,这里面有一个精妙的设计。即 java.time 类使用了序列化代理模式(Serialization Proxy Pattern)

复制代码
// LocalDate 的序列化代理实现
private Object writeReplace() {
    return new Ser(Ser.LOCAL_DATE_TYPE, this);
}

private Object readResolve() throws ObjectStreamException {
    throw new InvalidObjectException("A LocalDate proxy is required");
}

实际的序列化通过 java.time.Ser 类完成,该类实现了 Externalizable 接口,显式地编码和解码日期值,因此不受字段类型变更的影响。

问题示例

我们先看一个会出问题的代码(序列化 Class 对象)。

复制代码
// 在JDK 24中序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("localdate-class.ser"));
// 下面这个在JDK 25中会出问题
oos.writeObject(LocalDate.class);  
oos.close();

// 在JDK 25中反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("localdate-class.ser"));
// 下面的这行代码会抛出 InvalidClassException 异常
Class<?> clazz = (Class<?>) ois.readObject();  
ois.close();

下面我们来看一个序列化实例对象正常的代码。

复制代码
// 在JDK 24中序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("localdate-instance.ser"));
// 这个在任何版本都正常
oos.writeObject(LocalDate.now());  
oos.close();

// 在JDK 25中反序列化  
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("localdate-instance.ser"));
// 完美兼容
LocalDate date = (LocalDate) ois.readObject(); 
ois.close();

异常详情

当问题发生时,日志中可能会看到如下异常:

复制代码
java.io.InvalidClassException: java.time.LocalDate; incompatible types for field day
    at java.base/java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2077)
    at java.base/java.io.ObjectStreamClass.checkClass(ObjectStreamClass.java:2049)
    at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:908)
    at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2335)
    at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:2208)
    at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2218)
    at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1768)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:547)

下面我们说一下官方的修复过程。

解决方案和修复过程

官方解决方案

经过社区反馈,OpenJDK 团队意识到了这个兼容性问题的严重性,决定回退 JDK-8334742 的变更,恢复受影响类的原始字段类型。

你看,官方评估过后,考虑到严谨性,首选的方案竟然是回退版本。要是国内企业,更可能是求快,赶鸭子上架,草草的改一下继续上线

修复的关键点

毕竟,退回还是最快最可靠最简单的修复方案了。后面我感觉还会继续改为 byte 类型,但具体时间未定,也可能在 JDK 26、27 中再现。

  1. 完全回退字段类型变更:将 byte 恢复为 short
  2. 保持 Valhalla 项目的兼容性:寻找其他方式支持值类特性
  3. 加强序列化兼容性测试:增加跨版本的序列化测试用例

临时解决方案

在官方注意到这个 bug 后,也有临时的解决方案。即,在修复发布之前,如果大家的应用受到此问题影响,可以采取下面 3 中临时方案。

  1. 避免序列化 Class 对象:改为序列化实例对象
  2. 使用自定义序列化:实现自己的序列化机制
  3. 保持版本一致:确保序列化和反序列化使用相同的 JDK 版本

受影响的应用场景

根据老外网友的讨论,了解到目前有一下一些框架受到影响。

  • 分布式缓存系统(如 Hazelcast、Apache Ignite)
  • RPC 框架中的类信息传输
  • 序列化框架(如 Kryo、FST)的兼容性层
  • 某些特定的反射工具库

最佳实践建议

尽量最小化颗粒度的序列化,以及避免序列化 Class 对象。

复制代码
// 推荐:序列化具体的数据对象
public class DateEvent implements Serializable {
    private LocalDate date;  // 序列化实例
    private String description;
}

// 避免:序列化Class对象
public class ClassHolder implements Serializable {
    private Class<?> dateClass;  // 可能引发兼容性问题
}

总结

实际上,JDK 25.0.1 共修复了 5 个 bug。如下图所示。

JDK-8367031 只是其中最显眼的一个罢了。它告诉我们,一个看似简单的字段类型变更,也可能引发意想不到的兼容性问题。

最后,切记,最先吃螃蟹是有风险的,即使在进行 JDK 升级时,也尽量做到充分的测试和评估是避免生产环境问题的关键。

另外JDK 25 作为最新的长期支持(LTS)版本,虽然带来了许多令人兴奋的新特性,但也引入了一些需要特别注意的重大兼容性变化。了解这些变化对于平稳升级至关重要。

下面这个表格汇总了目前已知的主要兼容性问题点,帮助您快速把握全局。

问题领域 具体表现 影响严重度 关键信息
架构支持 移除了对 32 位 x86 架构的支持 高(针对特定环境) 无法在 32 位 Linux x86 系统上运行 JDK 25
线程本地数据共享 引入 Scoped Values作为 ThreadLocal的现代替代方案 中(长期影响) 不立即破坏兼容性 ,但预示 ThreadLocal的未来使用方式变化
模块系统 新增 import module语法,可能引发命名冲突 低至中 若同时导入不同模块中的同名类,需显式指定以消除歧义
构造函数语法 允许在 super()this()调用前执行代码 低(行为变化) 改变了传统的构造函数执行模型,可能影响依赖此顺序的复杂继承结构
相关推荐
BestAns21 小时前
一文带你吃透 Java 反射机制
java·后端
wasp52021 小时前
AgentScope Java 核心架构深度解析
java·开发语言·人工智能·架构·agentscope
2501_916766541 天前
【Springboot】数据层开发-数据源自动管理
java·spring boot·后端
自在极意功。1 天前
MyBatis 动态 SQL 详解:从基础到进阶实战
java·数据库·mybatis·动态sql
软件管理系统1 天前
基于Spring Boot的便民维修管理系统
java·spring boot·后端
百***78751 天前
Step-Audio-2 轻量化接入全流程详解
android·java·gpt·php·llama
快乐肚皮1 天前
MySQL递归CTE
java·数据库·mysql·递归表达式
廋到被风吹走1 天前
【Spring】DispatcherServlet解析
java·后端·spring
廋到被风吹走1 天前
【Spring】PlatformTransactionManager详解
java·spring·wpf