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()调用前执行代码 低(行为变化) 改变了传统的构造函数执行模型,可能影响依赖此顺序的复杂继承结构
相关推荐
麦麦鸡腿堡2 小时前
Java_HashMap底层机制与原码解读
java·开发语言·jvm
草莓熊Lotso2 小时前
C++ 抽象类与多态原理深度解析:从纯虚函数到虚表机制(附高频面试题)
java·运维·服务器·开发语言·c++·人工智能·笔记
再玩一会儿看代码2 小时前
Ken的Java学习之路——Java中关于面向对象
java·开发语言·经验分享·python·学习
迦蓝叶2 小时前
通过 HelloWorld 深入剖析 JVM 启动过程
java·开发语言·jvm·aot·启动过程·helloword·leyden
q***31892 小时前
深入解析Spring Boot中的@ConfigurationProperties注解
java·spring boot·后端
m0_565611132 小时前
Java Stream流操作全解析
java·开发语言·算法
xiezhr2 小时前
接口开发,咱得整得“优雅”点
java·api·代码规范
bagadesu3 小时前
IDEA + Spring Boot 的三种热加载方案
java·后端