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 根目录的 Foo
和 Bar
类将在不了解 MR JAR 的 Java 运行时(即 Java 8 及更早版本)中使用,而来自 JAR 根目录的 Foo
和来自 META-INF/versions/9 的 Bar
将在 Java 9 及更高版本中使用。JAR 清单必须包含一个条目 Multi-Release: true
以指示该 JAR 是 MR JAR。
示例:获取当前进程的 ID
例如,假设我们有一个库,它定义了一个提供正在运行的进程(PID)ID 的类。PID 应由包含实际 PID 和描述 PID 提供者的字符串的描述符表示
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
的基础。
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以及它是从哪里检索到的。
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中。
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运行。
...
<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资源插件来完成此操作。
...
<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
条目。
...
<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中使用。
结论
虽然javac
、jar
、java
和其他JDK工具已经支持多版本JAR,但构建工具如Maven仍需要迎头赶上。幸运的是,目前可以使用一些插件来完成,但我希望Maven等工具很快将提供创建MR JAR的原生支持。
其他人也在思考创建MR JARs的问题。例如,可以查看我的同事David M. Lloyd的这篇帖子(点击查看)。David使用单独的Maven项目处理Java 9特定的类,然后使用Maven依赖插件将这些类复制回主项目中。我个人更倾向于将所有源代码放在一个单独的项目中,因为这让我觉得简单一些,尽管也存在一些问题。具体来说,如果你在IDE中配置了src/main/java和src/main/java9作为源目录,你会遇到关于重复类ProcessIdProvider
的错误。这可以忽略(如果你不需要修改它,也可以从IDE中移除src/main/java9作为源目录),但这可能会让一些人感到烦恼。
人们可以考虑将Java 9类放在另一个包中,例如java9.com.example
,然后在构建项目时使用Maven shade插件将它们重新定位到com.example
,尽管这样做似乎很费力,而且收益很小。最终,如果IDE也能添加对MR JARs和在同一项目中使用不同源和目标目录进行多编译的支持,那就更好了。
欢迎在下面的评论部分提供有关此或创建MR JARs的其他方法的反馈。本博客文章的完整源代码可以在GitHub上找到。