使用 Maven 构建多版本 JAR

发布者:    |       讨论

Java 9 带来了一个对库作者非常有用的新功能:多版本 JAR(JEP 238)。

多版本 JAR(MR JAR)可以包含同一类的一个多个变体,每个变体针对特定的 Java 版本。在运行时,将自动加载类的正确变体,具体取决于正在使用的 Java 版本。

这允许库作者在早期利用新的 Java 版本的同时,仍然保持与旧版本的兼容性。例如,如果你的库在变量上执行原子比较和设置操作,你可能目前正在使用 sun.misc.Unsafe 类。由于 Unsafe 从未打算用于 JDK 之外的用途,Java 9 带来了对 CAS 逻辑的支持替代方案,即 var handles。通过将你的库作为 MR JAR 提供,你可以在 Java 9 上受益于 var handles,同时在旧平台上坚持使用 Unsafe

以下我们将讨论如何使用 Apache Maven 创建 MR JAR。

多版本 JAR 的结构

多版本 JAR 包含多个类文件树。主树位于 JAR 的根目录,而版本特定的树位于 META-INF/versions 下,例如

JAR root
- Foo.class
- Bar.class
+ META-INF
   - MANIFEST.MF
   + versions
      + 9
         - Bar.class

在这里,来自 JAR 根目录的 FooBar 类将在不了解 MR JAR 的 Java 运行时(即 Java 8 及更早版本)中使用,而来自 JAR 根目录的 Foo 和来自 META-INF/versions/9Bar 将在 Java 9 及更高版本中使用。JAR 清单必须包含一个条目 Multi-Release: true 以指示该 JAR 是 MR JAR。

示例:获取当前进程的 ID

例如,假设我们有一个库,它定义了一个提供正在运行的进程(PID)ID 的类。PID 应由包含实际 PID 和描述 PID 提供者的字符串的描述符表示

src/main/java/com/example/ProcessIdDescriptor.java
package com.example;

public class ProcessIdDescriptor {

    private final long pid;
    private final String providerName;

    // constructor, getters ...
}

在Java 8之前,没有简单的方法可以获取正在运行进程的ID。一个相当复杂的方法是解析RuntimeMXBean#getName()的返回值,在OpenJDK / Oracle JDK实现中是"pid@hostname"。虽然这种行为不能保证在所有实现中都通用,但让我们将其作为我们默认的ProcessIdProvider的基础。

src/main/java/com/example/ProcessIdProvider.java
package com.example;

public class ProcessIdProvider {

    public ProcessIdDescriptor getPid() {
        String vmName = ManagementFactory.getRuntimeMXBean().getName();
        long pid = Long.parseLong( vmName.split( "@" )[0] );
        return new ProcessIdDescriptor( pid, "RuntimeMXBean" );
    }
}

此外,让我们创建一个简单的主类来显示PID以及它是从哪里检索到的。

src/main/java/com/example/Main.java
package com.example;

public class Main {

    public static void main(String[] args) {
        ProcessIdDescriptor pid = new ProcessIdProvider().getPid();

        System.out.println( "PID: " + pid.getPid() );
        System.out.println( "Provider: " + pid.getProviderName() );
    }
}

注意到目前为止创建的源文件都位于常规的src/main/java源目录中。

现在,让我们基于Java 9的新ProcessHandle API创建ProcessIdDescriptor的另一个变体,它最终提供了一个获取当前PID的通用方式。此源文件位于另一个源目录src/main/java9中。

src/main/java9/com/example/ProcessIdProvider.java
package com.example;

public class ProcessIdProvider {

    public ProcessIdDescriptor getPid() {
        long pid = ProcessHandle.current().getPid();
        return new ProcessIdDescriptor( pid, "ProcessHandle" );
    }
}

设置构建

所有源文件就绪后,是时候配置Maven以便构建一个MR JAR了。

为此需要三个步骤。第一步是编译位于src/main/java9下的额外Java 9源文件。我希望我能够简单地为那个设置另一个Maven编译插件的执行,但我找不到一种方法,它只会编译src/main/java9而不是第二次编译src/main/java中的文件。

作为一个解决方案,可以使用Maven Antrun插件来配置一个仅针对Java 9特定源文件的第二个javac运行。

pom.xml
...
<properties>
    <java9.sourceDirectory>${project.basedir}/src/main/java9</java9.sourceDirectory>
    <java9.build.outputDirectory>${project.build.directory}/classes-java9</java9.build.outputDirectory>
</properties>
...
<build>
    ...
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
                <execution>
                    <id>compile-java9</id>
                    <phase>compile</phase>
                    <configuration>
                        <tasks>
                            <mkdir dir="${java9.build.outputDirectory}" />
                            <javac srcdir="${java9.sourceDirectory}" destdir="${java9.build.outputDirectory}"
                                classpath="${project.build.outputDirectory}" includeantruntime="false" />
                        </tasks>
                    </configuration>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
    ...
</build>
...

这使用target/classes目录(包含默认编译生成的类文件)作为类路径,允许引用所有Java版本都支持的公共类,例如ProcessIdDescriptor。编译后的类放入target/classes-java9

下一步是将编译的Java 9类复制到target/classes中,以便它们在生成的JAR中放置在正确的位置。可以使用Maven资源插件来完成此操作。

pom.xml
...
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-resources</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.outputDirectory}/META-INF/versions/9</outputDirectory>
                <resources>
                    <resource>
                        <directory>${java9.build.outputDirectory}</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>
...

这将把Java 9类文件从target/classes-java9复制到target/classes/META-INF/versions/9

最后,需要配置Maven JAR插件,以便在清单文件中添加Multi-Release条目。

pom.xml
...
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Multi-Release>true</Multi-Release>
                <Main-Class>com.example.Main</Main-Class>
            </manifestEntries>
        </archive>
        <finalName>mr-jar-demo.jar</finalName>
    </configuration>
</plugin>
...

这样,我们就有了构建多版本JAR所需的所有内容。通过mvn clean package(使用Java 9)触发构建,以在target目录中创建JAR。

为了查看JAR内容是否正确,使用jar -tf target/mr-jar-demo.jar列出其内容。你应该看到以下内容

...
com/example/Main.class
com/example/ProcessIdDescriptor.class
com/example/ProcessIdProvider.class
META-INF/versions/9/com/example/ProcessIdProvider.class
...

最终,让我们通过java -jar target/mr-jar-demo.jar执行JAR并检查其输出。当使用Java 8或更早版本时,你会看到以下内容

PID: <some pid>
Provider: RuntimeMXBean

而在Java 9上,它将是这样的

PID: <some pid>
Provider: ProcessHandle

即JAR根目录中的ProcessIdProvider类将在Java 8及更早版本中使用,而来自META-INF/versions/9的类将在Java 9中使用。

结论

虽然javacjarjava和其他JDK工具已经支持多版本JAR,但构建工具如Maven仍需要迎头赶上。幸运的是,目前可以使用一些插件来完成,但我希望Maven等工具很快将提供创建MR JAR的原生支持。

其他人也在思考创建MR JARs的问题。例如,可以查看我的同事David M. Lloyd的这篇帖子(点击查看)。David使用单独的Maven项目处理Java 9特定的类,然后使用Maven依赖插件将这些类复制回主项目中。我个人更倾向于将所有源代码放在一个单独的项目中,因为这让我觉得简单一些,尽管也存在一些问题。具体来说,如果你在IDE中配置了src/main/javasrc/main/java9作为源目录,你会遇到关于重复类ProcessIdProvider的错误。这可以忽略(如果你不需要修改它,也可以从IDE中移除src/main/java9作为源目录),但这可能会让一些人感到烦恼。

人们可以考虑将Java 9类放在另一个包中,例如java9.com.example,然后在构建项目时使用Maven shade插件将它们重新定位到com.example,尽管这样做似乎很费力,而且收益很小。最终,如果IDE也能添加对MR JARs和在同一项目中使用不同源和目标目录进行多编译的支持,那就更好了。

欢迎在下面的评论部分提供有关此或创建MR JARs的其他方法的反馈。本博客文章的完整源代码可以在GitHub上找到


返回顶部