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 或更高版本进行编译:

  1. 编译 Java 7 兼容版本:
javac --release 7 -d classes src\main\java\com\baeldung\multireleaseapp\*.java
  1. 编译 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


原始标题:Multi-Release Jar Files