1. 简介
Java 语言持续演进,JDK 不断引入新特性。如果我们想在自己的 API 中使用这些新特性,可能会迫使下游依赖升级 JDK 版本。
很多时候,我们不得不推迟使用新语言特性,以保证向后兼容性。
不过,从 Java 9 开始,我们有了一个解决方案:多版本 JAR(Multi-Release JAR,简称 MRJAR)。它允许一个 JAR 文件同时包含多个 JDK 版本兼容的实现。
2. 示例场景
假设我们有一个工具类 DateHelper
,其中有一个方法用于判断某一年是否为闰年。这个类最初是用 JDK 7 编写的,并且设计为能在 JRE 7 及以上版本运行:
public class DateHelper {
public static boolean checkIfLeapYear(String dateStr) throws Exception {
logger.info("Checking for leap year using Java 1 calendar API ");
Calendar cal = Calendar.getInstance();
cal.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(dateStr));
int year = cal.get(Calendar.YEAR);
return (new GregorianCalendar()).isLeapYear(year);
}
}
该方法在主类中被调用:
public class App {
public static void main(String[] args) throws Exception {
String dateToCheck = args[0];
boolean isLeapYear = DateHelper.checkIfLeapYear(dateToCheck);
logger.info("Date given " + dateToCheck + " is leap year: " + isLeapYear);
}
}
时至今日,我们知道 Java 8 提供了更简洁的日期解析方式。因此我们希望使用 Java 8 的新 API 重写逻辑。✅ 但问题来了:如果切换到 JDK 8+,这个模块就无法在原本支持的 JRE 7 上运行了。
我们当然不希望为了使用新特性而牺牲兼容性,除非万不得已。
3. 多版本 JAR 的解决方案
Java 9 引入了 MRJAR 机制来解决这个问题:
✅ 保留原始类不变,同时为新 JDK 版本创建新的类实现,然后将它们打包到同一个 JAR 文件中。
运行时,JVM(9 及以上版本)会根据自身版本选择加载对应的类实现,优先选择版本最高的那个。
举个例子:如果一个 MRJAR 包含了 Java 7(默认)、Java 9 和 Java 10 的实现,那么:
- JVM 10+ 会加载 Java 10 的版本
- JVM 9 会加载 Java 9 的版本
- 而 Java 7/8 的 JVM 会使用默认版本
⚠️ 注意:新版本类的 public 接口定义必须与原始类完全一致。也就是说,不能为某个版本新增 public 方法。
4. 目录结构规划
由于 Java 类名与文件路径一一对应,不能在同一个目录下存在两个同名类。因此我们需要为不同版本的类分别存放。
我们可以在 src/main
下创建一个 java9
目录,与 java
平级,并复制 DateHelper.java
到该目录下,保持包结构一致:
src/
main/
java/
com/
baeldung/
multireleaseapp/
App.java
DateHelper.java
java9/
com/
baeldung/
multireleaseapp/
DateHelper.java
⚠️ 一些不支持 MRJAR 的 IDE 可能会提示类重复错误。
关于如何与 Maven 等构建工具集成,我们会在后续文章中讲解。这里我们先聚焦基本原理。
5. 代码改动
我们来修改 java9
目录下的 DateHelper
类,使用 Java 8+ 的 API 重写逻辑:
public class DateHelper {
public static boolean checkIfLeapYear(String dateStr) throws Exception {
logger.info("Checking for leap year using Java 9 Date Api");
return LocalDate.parse(dateStr).isLeapYear();
}
}
✅ 注意事项:
- ✅ 不修改 public 方法签名
- ✅ 不新增 public 方法
- ❌ 否则打包会失败
6. 跨版本编译
Java 支持跨版本编译(cross-compilation),即使用高版本 JDK 编译出低版本可运行的字节码,而无需安装多个 JDK。
我们使用 JDK 9 或更高版本进行编译:
- 编译 Java 7 兼容版本:
javac --release 7 -d classes src\main\java\com\baeldung\multireleaseapp\*.java
- 编译 Java 9 版本:
javac --release 9 -d classes-9 src\main\java9\com\baeldung\multireleaseapp\*.java
--release
参数用于指定编译目标版本。
7. 创建 MRJAR 文件
使用 JDK 9+ 的 jar
工具创建多版本 JAR 文件:
jar --create --file target/mrjar.jar --main-class com.baeldung.multireleaseapp.App
-C classes . --release 9 -C classes-9 .
其中 --release 9
表示将 classes-9
下的内容打包进 JAR 的 META-INF/versions/9/
目录中。
最终生成的 JAR 结构如下:
com/
baeldung/
multireleaseapp/
App.class
DateHelper.class
META-INF/
versions/
9/
com/
baeldung/
multireleaseapp/
DateHelper.class
MANIFEST.MF
并在 MANIFEST.MF
中添加:
Multi-Release: true
这样,JVM 会识别这是一个 MRJAR,并在运行时加载对应版本的类。
老版本 JVM 会忽略这个属性,将其当作普通 JAR 处理。
8. 测试验证
Java 7/8 环境运行:
> java -jar target/mrjar.jar "2012-09-22"
Checking for leap year using Java 1 calendar API
Date given 2012-09-22 is leap year: true
Java 9+ 环境运行:
> java -jar target/mrjar.jar "2012-09-22"
Checking for leap year using Java 9 Date Api
Date given 2012-09-22 is leap year: true
✅ 表明不同版本 JVM 正确加载了对应版本的类。
9. 小结
通过本文的示例,我们了解了如何创建和使用多版本 JAR 文件。它为库开发者提供了一种优雅的向后兼容方案,使得可以在不破坏旧环境的前提下使用新特性。
📌 示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-9-new-features