在 Java 生态系统中,“向后兼容性"一直是一把双刃剑。一方面,它保证了旧代码能在新版本 JDK 上平稳运行;另一方面,对于库(Library)维护者来说,为了支持还在使用 Java 8 的用户,往往不得不放弃 Java 11、17 甚至 21 中引入的高效 API。

为了解决这个矛盾,Java 9 引入了 JEP 238: Multi-Release JAR Files (MRJAR)。它允许在一个 JAR 包中针对不同的 Java 版本存放同一份类的不同实现。

什么是 Multi-Release JAR?#

简单来说,Multi-Release JAR 允许你的库在不同的 JDK 版本上表现出不同的行为:

  • 在旧版本 JDK(如 Java 8)上运行时,它执行基础版本的代码。
  • 在新版本 JDK(如 Java 17)上运行时,它自动选择针对该版本优化的代码。

这一切对用户是透明的,他们只需要像往常一样引用一个 JAR 包即可。

MRJAR 的内部结构#

一个 Multi-Release JAR 的秘密全在 META-INF 目录下。它的标准结构如下:

example.jar
├── com/meirong/Helper.class         (基础版本,例如 Java 8)
├── META-INF/
│   ├── MANIFEST.MF                  (必须包含 Multi-Release: true)
│   └── versions/
│       ├── 11/
│       │   └── com/meirong/Helper.class (针对 Java 11+ 的优化版)
│       └── 17/
│           └── com/meirong/Helper.class (针对 Java 17+ 的优化版)

关键点:

  1. MANIFEST.MF: 必须在主属性中声明 Multi-Release: true
  2. versions 目录: 只有当运行环境的 JDK 版本大于或等于该目录名时,JVM 才会优先加载这里的类。

实战案例:获取进程 ID (PID)#

在 Java 9 之前,获取当前进程 ID 是一件非常痛苦的事情,通常需要解析 ManagementFactory 返回的字符串。而 Java 9 引入了简洁的 ProcessHandle API。

1. 基础版本 (src/main/java - Java 8)#

package com.meirong;
import java.lang.management.ManagementFactory;

public class ProcessUtils {
    public static long getPid() {
        // Java 8 时代的 Hack 手法: "pid@hostname"
        String name = ManagementFactory.getRuntimeMXBean().getName();
        return Long.parseLong(name.split("@")[0]);
    }
}

2. 优化版本 (src/main/java9 - Java 9+)#

package com.meirong;

public class ProcessUtils {
    public static long getPid() {
        // Java 9 引入的原生 API
        return ProcessHandle.current().pid();
    }
}

当这个 JAR 在 Java 8 上运行时,它会调用第一个实现;在 Java 11 或更高版本上运行时,它会自动切换到第二个实现。

如何构建 MRJAR?#

手动维护目录结构非常繁琐,主流构建工具都提供了很好的支持。

Maven 配置#

使用 maven-compiler-plugin 结合多执行期(executions)来实现:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.13.0</version>
    <executions>
        <execution>
            <id>java9-compile</id>
            <phase>compile</phase>
            <goals><goal>compile</goal></goals>
            <configuration>
                <release>9</release>
                <compileSourceRoots>
                    <compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
                </compileSourceRoots>
                <multiReleaseOutput>true</multiReleaseOutput>
            </configuration>
        </execution>
    </executions>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Multi-Release>true</Multi-Release>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Gradle 配置#

在 Gradle 中,可以通过定义不同的 sourceSets 并配置 Jar 任务来完成:

sourceSets {
    val java9 by creating {
        java.srcDir("src/main/java9")
    }
}

tasks.jar {
    manifest {
        attributes["Multi-Release"] = "true"
    }
    into("META-INF/versions/9") {
        from(sourceSets["java9"].output)
    }
}

核心限制与注意事项#

虽然 MRJAR 非常强大,但它有一些严格的规则:

  1. API 必须一致: 不同版本的同一个类,其 Public API(方法名、参数、返回值、可见性)必须完全一致。MRJAR 旨在提供不同的 实现,而不是不同的 接口
  2. 类路径阴影: 如果在 versions/ 下定义的类在基础路径下不存在,JVM 可能会忽略它。
  3. 测试挑战: 你需要在多个不同的 JDK 版本上运行测试,以确保所有版本的代码路径都能正常工作。

总结#

Multi-Release JAR 是库开发者的一大神器。它让我们不再受限于最旧的受支持版本,能够大胆地拥抱现代 JDK 带来的性能和 API 优势。在 Java 21+ 逐渐成为主流的今天,MRJAR 依然是维持生态平衡的重要工具。


参考资料: