第 10 章 库迁移

Chapter 10. Library Migration

The previous chapters focused on migrating applications to the module system. Many of these lessons apply when migrating an existing library as well. Still, several issues affect library migration more than application migration. In this chapter, we identify those issues and their solutions.

前面的章节着重于将应用程序迁移到模块系统。如果是迁移一个现有库,那么很多内容也是适用的。但是与应用程序迁移相比,还有几个问题影响了库迁移,本章将会介绍这些问题及其解决方案。

The biggest difference between migrating a library and migrating an application is that libraries are used by many applications. These applications may run on different versions of Java, so libraries often need to work across a range of Java versions. It’s not realistic to expect users of your library to switch to Java 9 at the same time your library switches. Fortunately, a combination of new features in Java 9 enables a seamless experience for both library maintainers and users.

迁移库和迁移应用程序之间最大的区别在于库被许多应用程序使用。这些应用程序可能运行在不同版本的 Java 上,所以库通常需要在各种 Java 版本上工作。期望库的用户在你迁移库的同时切换到 Java 9 是不现实的。幸运的是,Java 9 中的新功能组合为库维护人员和用户提供了无缝的体验。

The goal is to migrate an existing library in incremental steps to a modular library. You don’t have to be the author of a popular open source project for this to be interesting. If you write code that is shared with other teams in a company, you’re in the same boat.

本章的目标是将现有库逐步迁移为模块化的库。但你并不需要为了兴趣而成为流行开源项目的作者。如果编写与公司其他团队共享的代码,那么你和他们就在同一条船上。

A migration process for libraries consists of the following steps:

库的迁移过程由以下步骤组成:

  1. Ensure that the library can run as an automatic module on Java 9.
  2. Compile the library with the Java 9 compiler (targeting your desired minimum Java version), without using new Java 9 features just yet.
  3. Add a module descriptor and turn the library into an explicit module.
  4. Optionally, refactor the structure of the library to increase encapsulation, identify APIs, and possibly split into multiple modules.
  5. Start using Java 9 features in the library while maintaining backward compatibility with earlier versions of Java 9.

1)确保库可以作为自动模块在 Java 9 上运行。 2)使用 Java 9 编译器编译库(主要使用满足需求的最低 Java 版本),而不使用新的 Java 9 功能。 3)添加一个模块描述符,并将库转换为显式模块。 4)重构库的结构,以便增加封装性,识别 API,并尽可能分割成多个模块(可选)。 5)开始使用库中的 Java 9 功能,同时保持向后兼容 Java 9 的早期版本。

The second step is optional, but recommended. With the newly introduced —release flag, earlier versions of Java can be reliably targeted with the Java 9 compiler. In “Targeting Older Java Versions”, you’ll see how to use this option. Throughout all steps, backward compatibility can be maintained with earlier versions of Java. For the last step, this may be especially surprising. It’s made possible by a new feature, multi-release JARs, which we explore toward the end of this chapter.

虽然第 2 步是可选的,但建议完成该步骤。通过新引入的— release 标志,可以使用 Java 9 编译器可靠地针对 Java 的早期版本进行编译。在 10.5 节中将会介绍如何使用此选项。在所有步骤中,都可以保持与 Java 的早期版本的向后兼容性。最后一步可能是特别令人惊讶的,该步骤是使用本章最后所探讨的一个新特性(多版本 JAR)来实现的。

10.1 Before Modularization 模块化之前

As a first step, you need to ensure that your library can be used as is with Java 9. Many applications will be using your library on the classpath, even on Java 9. Furthermore, library maintainers need to get their libraries in shape for use as automatic modules in applications. In many cases, no code changes are necessary. The only changes to make at this point are to prevent showstoppers, such as the use of encapsulated or removed types from the JDK.

首先,需要确保库可以与 Java 9 一起使用。许多应用程序都是在类路径上使用库,甚至在 Java 9 上也是如此。此外,库维护者还需要修改库,以便在应用程序中作为自动模块使用。在许多情况下是不需要更改代码的,唯一需要做的更改是防止“搅局者”(showstopper),比如使用 JDK 中的封装或删除类型。

Before making a library into a module (or collection of modules), you should take the same initial step as when migrating applications, as detailed in Chapter 7. Ensuring that the library runs on Java 9 means it should not use encapsulated types in the JDK. If it does use such types, library users are possibly faced with warnings, or exceptions (if they run with the recommended —illegal-access=deny setting). This forces them to use —add-opens or —add-exports flags. Even if you document this with your library, it’s not a great user experience. Usually the library is just one of many in an application, so tracking down all the right command-line flags is painful for users. It’s better to use jdeps to find uses of encapsulated APIs in the library and change them to the suggested replacement. Use jdeps -jdkinternals, as described in “Using jdeps to Find Removed or Encapsulated Types and Their Alternatives”, to quickly spot problems in this area. When these replacement APIs are only available starting with Java 9, you cannot directly use them while supporting earlier Java releases. In “Multi-Release JARs”, you’ll see how multi-release JARs can solve this problem.

在将一个库转变为一个模块(或模块集合)之前,应该像迁移应用程序一样采取相同的初始步骤(详见第 7 章)。确保在 Java 9 上运行库意味着它不应该使用 JDK 中的封装类型。如果使用这种类型,库用户可能会遇到警告或异常(如果它们使用推荐的—illegal-access = deny 设置运行应用程序)。这就迫使他们使用—add-opens 或—add-exports 标志。但即使在库中使用这些标志,也不是一个良好的用户体验。通常,库只是应用程序中众多应用中的一个,因此,追踪所有正确的命令行标志对用户来说是非常痛苦的。最好是使用 jdeps 来查找库中封装 API 的使用,并将其更改为建议的替换内容。如“使用 jdeps 查找已删除或封装的类型及其替代方法”内容中所述,可以使用 jdeps -jdkinternals 快速发现相关问题。如果这些替换 API 仅从 Java 9 之后开始可用,那么当需要支持较早的 Java 版本时就不能直接使用它们。在 10.7.1 节中,将会学习多版本 JAR 如何解决此问题。

At this stage, we’re not yet creating a module descriptor for the library. We can postpone thinking about what packages need to be exported or not. Also, any dependencies the library has on other libraries can be left implicit. Whether the library is put on the classpath, or on the module path as an automatic module, it can still access everything it needs without explicit dependencies.

目前,还没有为库创建模块描述符,可以推迟考虑需要导出哪些包。另外,库对其他库的依赖关系也可以是隐式的。无论该库是放在类路径上,还是作为自动模块放在模块路径上,都可以访问所需的所有内容,而无须显式的依赖关系。

After this step, the library can be used on Java 9. We’re not using any new features from Java 9 in the library implementation yet. In fact, if there are no uses of encapsulated or removed APIs, there’s no need to even recompile the library.

在上述步骤之后,就可以在 Java 9 上使用该库了,目前在库的实现中没有使用 Java 9 中的任何新特性。事实上,如果没有使用封装或删除的 API,甚至不需要重新编译库。

10.2 Choosing a Library Module Name 选择库模块名称

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

计算机科学中只存在两个难题:缓存失效和命名问题。—Phil Karlton

It is important at this point to think about the name your module should have when it becomes a real module later. What is a good name for a library module? On the one hand, you want the name to be simple and memorable. On the other hand, we are talking about a widely reusable library, so the name must be unique. There can be only one module on the module path for any given name.

此时应该重点考虑的是模块在以后变成真正的模块时应该有的名称。对于库模块来说,什么是好名称呢?一方面,希望名称简单而难忘。另一方面,由于正在讨论的是一个可广泛重用的库,所以名称必须是独一无二的。对于任何给定的名称,模块路径上只能有一个模块。

A long-standing tradition for making globally unique names in the Java world is to use the reverse DNS notation. When you have a library called mylibrary, its module name could be com.mydomain.mylibrary. Applying this naming convention to non-reusable application modules is unnecessary, but with libraries, the extra visual noise is warranted. Several open source Java libraries, for example, are named spark. If no precautions are taken by these library maintainers, they may end up claiming the same module name. That would mean applications could no longer use those libraries together in an application. Claiming a reverse DNS–based module name is the best way to prevent clashes.

在 Java 世界中创建全球唯一名称的传统方法是使用反向 DNS 表示法。当有一个名为 mylibrary 的库时,其模块名可以是 com.mydomain.mylibrary。将这种命名约定应用于不可复用的应用程序模块是没有必要的,但是对于库来说,额外的视觉噪声是很有必要的。例如,将几个开源 Java 库命名为 spark。如果这些库维护人员没有采取预防措施,那么他们可能会声明相同的模块名称,这意味着应用程序不能在程序中一起使用这些库。声明一个反向的基于 DNS 的模块名是防止此类冲突的最好方法。

TIP

A good candidate for a module name is the longest common prefix of all packages in the module. Assuming reverse-DNS package names, this top-level package name is a natural module identifier. In Maven terms, this often means combining the group ID and artifact ID as module name.

模块名称的最佳候选者是模块中所有包的最长公共前缀。假设使用反向 DNS 包名称,顶级包名称就是一个自然的模块标识符。在 Maven 术语中,这通常意味着将组 ID 和工件 ID 组合为模块名称。

Digits are allowed in module names. It may be tempting to add a version number in the module name (e.g., com.mydomain.mylibrary2). Don’t do this. Versioning is a separate concern from identifying your library. A version can be set when creating the modular JAR (as discussed in “Versioned Modules”), but should never be part of the module name. Just because you upgrade your library with a major version doesn’t mean the identification of your library should change. Some popular libraries already painted themselves into this corner a long time ago. The Apache commons-lang library, for example, decided on the commons-lang3 name when moving from version 2 to 3. Currently, versions belong to the realm of build tools and artifact repositories, not the module system. An updated version of a library should not lead to changes in module descriptors.

数字在模块名称中是允许的。虽然在模块名称中添加一个版本号(例如,com.mydomain.mylibrary2)可能会更有吸引力,但是不要这样做。版本控制是识别库的另一个独立问题。虽然创建模块化 JAR 时可以设置版本信息(如 5.7 节中所述),但不应该是模块名称的一部分。因为用一个主版本来升级库并不意味着库的标识应该改变。很久以前,一些流行的库已经陷入了这种困境中。例如,Apache commons-lang 库在从版本 2 迁移到版本 3 时使用了 commons-lang3 名称。目前,版本控制属于构建工具和工件存储库领域,而不是模块系统。库的更新版本不应导致模块描述符的更改。

You’ve learned that automatic modules derive their name from the JAR filename in Chapter 8. Unfortunately, the ultimate filename is often dictated by build tools or other processes that are out of the hands of library maintainers. Applications using the library as an automatic module then require your library through the derived name in their module descriptors. When your library switches to being an explicit module later, you’re stuck with this derived name. That’s a problem when the derived filename isn’t quite correct or uniquely identifying. Or worse, when different people use different filenames for your library. Expecting every application that uses the library to later update their requires clauses to your new module name is unrealistic.

在第 8 章中曾经讲过,自动模块的名称来自 JAR 文件名。但不幸的是,最终的文件名通常是由构建工具或其他过程所确定的,而库维护者无法控制这些工具或过程。将库作为自动模块使用的应用程序通过模块描述符中的派生名请求库。当库日后切换成一个显式模块时,你就会被这个派生名所困扰。当派生文件名不完全正确或不是唯一标识时,就会出现问题。当不同的人使用库的不同文件名时,情况就更糟糕了。期望每个使用该库的应用程序将它们的 requires 子句更新为新的模块名称是不现实的。

That puts library maintainers in an awkward position. Even though the library itself is not a module yet, it will be used as such through the automatic modules feature. And when applications start doing so, you’re effectively stuck with an automatically derived module name that may or may not be what you want it to be. To solve this conundrum, an alternative way of reserving a module name is possible.

这使得库维护人员处于一个尴尬的境地。尽管库本身不是一个模块,但它将通过自动模块功能被使用。当应用程序开始这样做时,实际上会被一个自动派生的模块名称所困扰,因为该名称可能并不是想要的。为了解决这个难题,可以采用保留模块名称的另一种方法。

Start by adding an Automatic-Module-Name: <module_name> entry to a nonmodular JAR in its META-INF/MANIFEST.MF. When the JAR is used as automatic module, it will assume the name as defined in the manifest, instead of deriving it from the JAR filename. Library maintainers can now define the name that the library module should have, without creating a module descriptor yet. Simply adding the new entry with the correct module name to MANIFEST.MF and repackaging the library is enough. The jar command has an -m <manifest_file> option telling it to add entries from a given file to the generated MANIFEST.MF in the JAR (➥ chapter10/modulename):

首先在 META-INF/MANIFEST.MF 中添加一个 Automatic-Module-Name:<module_name>条目到非模块化 JAR。当该 JAR 被用作自动模块时,它将采用清单中定义的名称,而不是从 JAR 文件名中派生名称。现在,库维护人员可以定义库模块应该具有的名称,而无须创建模块描述符。只需将具有正确模块名称的新条目添加到 MANIFEST.MF 并重新打包库就足够了。jar 命令有一个-m <manifest_file>选项,告诉它将来自给定文件的条目添加到 JAR 中所生成的 MANIFEST.MF 中(chapter10/modulename):

  1. jar -cfm mylibrary.jar src/META-INF/MANIFEST.MF -C out/ .

With this command, the entries from src/META-INF/MANIFEST.MF are added to the generated manifest in the output JAR.

通过使用该命令,来自 src/META-INF/MANIFEST.MF 的条目将被添加到输出 JAR 中生成的清单中。

TIP

With Maven, you can configure the JAR plug-in to add the manifest entry:

通过使用 Maven,可以配置 JAR 插件来添加清单条目:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-jar-plugin</artifactId>
  4. <configuration>
  5. <archive>
  6. <manifestEntries>
  7. <Automatic-Module-Name>
  8. com.mydomain.mylibrary
  9. </Automatic-Module-Name>
  10. </manifestEntries>
  11. </archive>
  12. </configuration>
  13. </plugin>

You’ll read more about Maven support for the module system in Chapter 11.

在第 11 章,将会了解更多关于 Maven 对模块系统的支持。

Reserving a module name with Automatic-Module-Name in the manifest is something you should do as quickly as possible. Naming is hard, and picking a name should be done deliberately. However, after settling on a module name, reserving it with Automatic-Module-Name is straightforward. It is a low-effort, high-impact move: no code changes or recompilation necessary.

在清单中使用 Automatic-Module-Name 来保留模块名称是应该尽快完成的事情。命名是很难的,应该有意识地选一个名称。但是,在解决了模块名称之后,使用 Automatic-Module-Name 保留模块名称就比较简单了。这是一个省力且高效的方法:不需要更改代码或重新编译。

WARNING

Add Automatic-Module-Name to your library’s manifest only if you verified that it works on JDK 9 as an automatic module. The existence of this manifest entry signals Java 9 compatibility. Any migration issues described in earlier chapters must be solved before promoting the use of your library as a module.

只有在确保库可以在 JDK 9 上作为自动模块运行时,才能将 Automatic-Module-Name 添加到库的清单中。该清单条目的存在标志着 Java 9 的兼容性。在将库作为模块使用之前,必须解决前面章节中所描述的任何迁移问题。

Why not create a module descriptor instead? There are several reasons.

为什么不创建一个模块描述符?有以下几个原因。

First, it involves thinking about what packages to expose or not. You can export everything explicitly, as would happen implicitly when the library is used as an automatic module. However, sanctioning this behavior explicitly in your module descriptor is something you cannot easily take back later. People using your library as an automatic module know that access to all packages is a side effect of it being an automatic module, subject to change later.

首先,要考虑公开哪些包。可以显式地导出所有内容,就像将库用作自动模块时那样。但是,如果在模块描述符中显式地导出所有内容,那么这种行为是不能轻易收回的。将库用作自动模块的人都知道,可以访问所有包是自动模块的副作用,稍后可能会更改。

Second, and more important, your library can have external dependencies itself. With a module descriptor, your library is no longer an automatic module on the module path. That means it won’t automatically have a requires relation with all other modules and the classpath (unnamed module) anymore. All dependencies must be made explicit in the module descriptor. Those external dependencies may not yet be modularized themselves, which prevents your library from having a correct module descriptor. Never publish your library with a dependency on an automatic module if that dependency doesn’t at least have the Automatic-Module-Name manifest entry yet. The name of such a dependency is unstable in that case, causing your module descriptor to be invalid when the dependency’s (derived) module name changes

其次,也是更重要的原因,库本身可能存在外部依赖。如果使用了模块描述符,那么库不再是模块路径上的自动模块。这意味着它不会自动与所有其他模块和类路径(未命名模块)存在一个 requires 关系。必须在模块描述符中显式确定所有的依赖关系。而那些外部依赖项可能还没有被模块化,这样一来就会阻碍库拥有正确的模块描述符。如果一个依赖项还没有 Automatic-Module-Name 清单条目,那么就不要使用自动模块上的该依赖项来发布库。因为此时依赖项的名称不稳定,当依赖的(派生的)模块名称改变时,就会导致模块描述符无效。

Last, a module descriptor must be compiled with the Java 9 compiler. These are all significant steps that take time to get right. Reserving the module name with a simple Automatic-Module-Name entry in the manifest before taking all these steps is sensible.

最后,模块描述符必须用 Java 9 编译器进行编译。这些都是需要时间才能正确完成的重要步骤。在执行所有这些步骤之前,在清单中使用简单的 Automatic-Module-Name 条目保留模块名称是非常明智的。

10.3 Creating a Module Descriptor 创建模块描述符

Now that the library is properly named and usable with Java 9, it’s time to think about turning it into an explicit module. For now, we assume the library is a single JAR (mylibrary.jar) to be converted to a single module. Later, you may want to revisit the packaging of the library and split it up further.

现在,库已正确命名并可用于 Java 9,接下来可以考虑将其变成一个显式模块。先假设库是一个单一 JAR(mylibrary.jar),并转换为一个单一模块。之后,可能需要重新访问库的包装并进一步拆分。

NOTE

In “Library Module Dependencies”, we’ll look at more complex scenarios in which the library consists of multiple modules or has external dependencies.

在 10.6 节中将会看到更复杂的场景,其中库由多个模块组成或具有外部依赖关系。

You have two choices with regard to creating a module descriptor: create one from scratch, or use jdeps to generate one based on the current JAR. In any case, it’s important that the module descriptor features the same module name as chosen earlier for the Automatic-Module-Name entry in the manifest. This makes the new module a drop-in replacement for the old version of the library when it was used as an automatic module. With a module descriptor in place, the manifest entry can be dropped.

关于创建模块描述符,有两个选择:从头创建一个或者使用 jdeps 根据当前的 JAR 生成一个。不管使用哪种方法,最重要的是模块描述符拥有与之前在清单 Automatic-Module-Name 条目中所选择的模块名称相同的模块名称。当被用作自动模块时,这样做可以让新模块成为老版本库的替代品。如果使用模块描述符,则可以删除清单条目。

Our example mylibrary (➥ chapter10/generate_module_descriptor) is fairly simple and consists of two classes in two packages. The central class MyLibrary contains the following code:

示例 mylibrary(chapter10/generate_module_descriptor)非常简单,由两个包中的两个类组成。核心类 MyLibrary 包含以下代码:

  1. package com.javamodularity.mylibrary;
  2. import com.javamodularity.mylibrary.internal.Util;
  3. import java.sql.SQLException;
  4. import java.sql.Driver;
  5. import java.util.logging.Logger;
  6. public class MyLibrary {
  7. private Util util = new Util();
  8. private Driver driver;
  9. public MyLibrary(Driver driver) throws SQLException {
  10. Logger logger = driver.getParentLogger();
  11. logger.info("Started MyLibrary");
  12. }
  13. }

Functionally, it doesn’t really matter what this code does; the important part is in the imports. When creating a module descriptor, we need to establish what other modules we require. Visual inspection shows the MyLibrary class uses types from java.sql and java.logging in the JDK. The …internal.Util class comes from a different package in the same mylibrary.jar. Instead of trying to come up with the right requires clauses ourselves, we can use jdeps to list the dependencies for us. Besides listing the dependencies, jdeps can even generate an initial module descriptor:

从功能上讲,上述代码所完成的事情并不重要,重点是在导入部分。在创建模块描述符时,需要确定所需的其他模块。目测检查会发现 MyLibrary 类使用了 JDK 中来自 java.sql 和 java.logging 的类型。…internal.Util 类来自同一个 mylibrary. jar 中的不同包。可以使用 jdeps 来列出所有依赖项,而不要尝试自己写出正确的 requires 子句。除了列出依赖项之外,jdeps 甚至可以生成一个初始模块描述符:

  1. jdeps --generate-module-info ./out mylibrary.jar

This results in a generated module descriptor in out/mylibrary/module-info.java:

上述代码可以在 out/mylibrary/module-info.java 中生成模块描述符:

  1. module mylibrary {
  2. requires java.logging;
  3. requires transitive java.sql;
  4. exports com.javamodularity.mylibrary;
  5. exports com.javamodularity.mylibrary.internal;
  6. }

jdeps analyzes the JAR file and reports dependencies to java.logging and java.sql. Interestingly, the former gets a requires clause, whereas the latter gets a requires transitive clause. That’s because the java.sql types used in MyLibrary are part of the public, exported API. The java.sql.Driver type is used as an argument to the public constructor of MyLibrary. Types from java.logging, on the other hand, are used only in the implementation of MyLibrary and are not exposed to library users. By default, all packages are exported in the jdeps-generated module descriptor.

jdeps 分析 JAR 文件并将依赖关系报告给 java.logging 和 java.sql。有趣的是,前者生成了一个 requires 子句,而后者生成一个 requires transitive 子句。这是因为 MyLibrary 中使用的 java.sql 类型是公共导出的 API 的一部分。java.sql.Driver 类型用作 MyLibrary 公共构造函数的参数。另一方面,java. logging 中的类型仅用于 MyLibrary 的实现,不会向库用户公开。在默认情况下,所有包都会导出到 jdeps 生成的模块描述符中。

WARNING

When a library contains classes outside any package (in the unnamed package, colloquially known as the default package), jdeps produces an error. All classes in a module must be part of a named package. Even before modules, placing classes in the unnamed package was considered a bad practice—especially so for reusable libraries.

当库包含包之外的类(在未命名的包中,俗称默认包)时,jdeps 会产生一个错误。模块中的所有类都必须是命名包的一部分。即使在模块出现之前,将类放在未命名的包中也是一种不好的做法——尤其是对于可重用的库而言。

You may think this module descriptor provides the same behavior as when mylibrary is used as an automatic module. And that’s largely the case. However, automatic modules are open modules as well. The generated module descriptor doesn’t define an open module, nor does it open any packages. Users of the library will notice this only when doing deep reflection on types from mylibrary. When you expect users of your library to do this, you can generate an open module descriptor instead:

此时,你可能会认为这个模块描述符提供了与 mylibrary 被用作自动模块时一样的行为。在一定程度上讲是这样的。但是,自动模块也是开放式模块。而生成的模块描述符并没有定义开放式模块,也没有开放任何包。库的用户只有在对 mylibrary 的类型进行深度反射时才会注意到这个区别。如果希望库用户及时注意到这一点,可以生成一个开放的模块描述符:

  1. jdeps --generate-open-module ./out mylibrary.jar

This generates the following module descriptor:

上述代码生成如下所示的模块描述符:

  1. open module mylibrary {
  2. requires java.logging;
  3. requires transitive java.sql;
  4. }

All packages will be open because an open module is generated. No exports statements are generated by this option. If you add exports for all packages to this open module, its behavior is close to using the original JAR as an automatic module.

此时,所有的包将被开放,因为生成了一个开放式模块。但并没有生成 exports 语句。如果向该开放式模块添加所有包的 exports 语句,则其行为类似于将原始 JAR 用作自动模块。

It’s preferable to create a nonopen module, exporting only the minimum number of packages necessary. One of the main draws of turning a library into a module is to benefit from strong encapsulation, something an open module does not offer.

创建一个非开放式模块可能更好,即仅导出必要的最小数量的包。将库转换为模块的主要原因之一是强封装所带来的好处,这是开放式模块所不具备的。

TIP

You should always view the generated module descriptor as just a starting point.

应该始终将生成的模块描述符视为一个起点。

Exporting all packages rarely is the right thing to do. For the mylibrary example, it makes sense to remove exports com.javamodularity.mylibrary.internal. There is no need for users of mylibrary to depend on internal implementation details.

在大多数情况下,没有必要导出所有包。对于 mylibrary 示例,删除 exports com.javamodularity.mylibrary.internal 是有意义的。mylibrary 的用户不需要依赖内部的实现细节。

Furthermore, if your library uses reflection, jdeps won’t find those dependencies. You need to add the right requires clauses for modules you reflectively load from yourself. These can be requires static if the dependency is optional, as discussed in “Compile-Time Dependencies”. If your library uses services, these uses clauses must be added manually as well. Any services that are provided (through files in META-INF/services) are automatically picked up by jdeps and turned into provides .. with clauses.

此外,如果库使用了反射,jdeps 将不会找到这些依赖项,需要为反射加载的模块添加正确的 requires 子句。如 5.6.1 节所述,如果依赖关系是可选的,那么这些子句可以是 requires static。如果库使用了服务,则必须手动添加 uses 子句。所提供的任何服务(通过 META-INF/services 中的文件)都会被 jdeps 自动提取,并转换为 provides…with 子句。

Finally, jdeps suggests a module name based on the filename, as with automatic modules. The caveats discussed in “Choosing a Library Module Name” still apply. For libraries, it’s better to create a fully qualified name by using reverse-DNS notation. In this example, com.javamodularity.mylibrary is the preferred module name. When the JAR you’re generating the module descriptor from already contains an Automatic-Module-Name manifest entry, this name is suggested instead.

最后,jdeps 会根据文件名建议一个模块名称,就像自动模块一样。10.2 节讨论的注意事项此时仍然适用。对于库,最好使用反向 DNS 表示法来创建完全限定的名称。在本示例中,com.javamodularity.mylibrary 是首选的模块名称。当正在生成模块描述符的 JAR 已经包含 Automatic-Module-Name 清单条目时,建议使用此名称。

10.4 Updating a Library with a Module Descriptor 使用模块描述符更新库

After creating or generating a module descriptor, we’re left with a module-info.java that still needs to be compiled. Only Java 9 can compile module-info.java, but that does not mean you need to switch compilation to Java 9 for your whole project. In fact, it’s possible to update the existing JAR (compiled with an earlier Java version) with just the compiled module descriptor. Let’s see how that works for mylibrary.jar, where we take the generated module-info.java and add it:

在创建或生成模块描述符之后,留下了一个仍然需要编译的 module-info.java。只有 Java 9 可以编译 module-info.java,但这并不意味着需要将整个项目的编译切换到 Java 9。事实上,可以用已编译的模块描述符来更新现有的 JAR(使用较早的 Java 版本编译)。接下来看一下 mylibrary.jar 是如何工作的,此时生成并添加 module-info.java:

  1. mkdir mylibrary
  2. cd mylibrary
  3. jar -xf ../mylibrary.jar 1
  4. cd ..
  5. javac -d mylibrary out/mylibrary/module-info.java 2
  6. jar -uf mylibrary.jar -C mylibrary module-info.class 3
  1. Extract the class files into ./mylibrary.
  2. Compile just module-info.java with the Java 9 compiler into the same directory as the extracted classes.
  3. Update the existing JAR file with the compiled module-info.class.

  1. 将类文件提取到./mylibrary。
  2. 使用 Java 9 编译器将 module-info.java 编译到与所提取类相同的目录中。
  3. 使用已编译的 module-info.class 更新现有的 JAR 文件。

With these steps, you can create a modular JAR from a pre-Java 9 JAR. The module descriptor is compiled into the same directory as the extracted classes. This way, javac can see all the existing classes and packages that are mentioned in the module descriptor, so it won’t produce errors. It’s possible to do this without having access to the sources of the library. No recompilation of existing code is necessary—unless, of course, code needs to be changed, for example, to avoid use of encapsulated JDK APIs.

通过以上步骤,可以由 Java 9 之前的 JAR 创建模块化的 JAR。模块描述符被编译到与所提取类相同的目录中,这样一来,javac 可以看到模块描述符中所提到的所有现有类和包,所以它不会产生错误。无须访问库的来源就做到这一点是可能的。没有必要重新编译现有的代码,当然除非需要改变代码,例如为了避免使用封装的 JDK API。

After these steps, the resulting JAR file can be used in various setups:

在完成上述步骤之后,可以在各种设置中使用生成的 JAR 文件:

  • On the classpath in pre-Java 9 versions
  • On the module path in Java 9 and later
  • On the classpath in Java 9

  • 在 Java 9 之前版本的类路径上使用。
  • 在 Java 9 以及后续版本的模块路径上使用。
  • 在 Java 9 的类路径上使用。

The compiled module descriptor is ignored when the JAR is put on the classpath in earlier Java versions. Only when the JAR is used on the module path with Java 9 or later does the module descriptor come into play.

如果将 JAR 放在早期 Java 版本的类路径中,那么将忽略已编译的模块描述符。只有在 Java 9 或更高版本的模块路径上使用 JAR 时,模块描述符才会起作用。

10.5 Targeting Older Java Versions 针对较旧的 Java 版本

What if you need to compile the library sources as well as the module descriptor? In many cases, you’ll want to target a Java release before 9 with your library. You can achieve this in several ways. The first is to use two JDKs to compile the sources and the module descriptor separately.

如果需要编译库的源文件以及模块描述符,那么又该怎么办呢?在很多情况下,需要针对 Java 9 之前的 Java 版本完成该操作。可以通过几种方式来实现。首先是使用两个 JDK 分别编译库源和模块描述符。

Let’s say we want mylibrary to be usable on Java 7 and later. In practice, this means the library source code can’t use any language features introduced after Java 7, nor any APIs added after Java 7. By using two JDKs, we can ensure that our library sources don’t depend on Java 7+ features, while still being able to compile the module descriptor:

假设希望 mylibrary 在 Java 7 及更高版本上可用。实际上,这意味着库的源代码不能使用 Java 7 之后引入的任何语言功能,也不能使用 Java 7 之后添加的任何 API。通过使用两个 JDK,可以确保库的源代码不依赖于 Java 7+功能,同时仍然能够编译模块描述符:

  1. jdk7/bin/javac -d mylibrary <all sources except module-info>
  2. jdk9/bin/javac -d mylibrary src/module-info.java

Again, it’s essential for both compilation runs to target the same output directory. The resulting classes can then be packaged into a modular JAR just as in the previous example. Managing multiple JDKs can be a bit cumbersome. A new feature, added in JDK 9, allows the use of the latest JDK to target an earlier version.

同样,对于两个编译运行来说,输出到同一个目录是至关重要的。这样一来,可以像前面的示例一样,将生成的类打包成模块化的 JAR。但管理多个 JDK 可能有点麻烦。在 JDK 9 中添加了一项新功能,允许针对较早的版本使用最新的 JDK。

The mylibrary example can be compiled using the new —release flag with just JDK 9:

可以使用 JDK 9 包含的—release 新标志来编译 mylibrary 示例:

  1. jdk9/bin/javac --release 7 -d mylibrary <all sources except module-info>
  2. jdk9/bin/javac --release 9 -d mylibrary src/module-info.java

This new flag is guaranteed to support at least three major previous releases from the current JDK. In the case of JDK 9, this means you can compile toward JDK 6, 7, and 8. As an added bonus, you get to benefit from bug fixes and optimizations in the JDK 9 compiler even when your library itself targets earlier releases. If you need to support even earlier versions of Java, you can always fall back to using multiple JDKs.

这个新标志保证至少支持当前 JDK 的前三个主要版本。在 JDK 9 的情况下,这意味着可以针对 JDK 6、7 和 8 进行编译。这样做的额外好处是,即使库是针对早期版本开发的,也可以受益于 JDK 9 编译器中的错误修复和优化。如果需要支持更早版本的 Java,则随时可以使用多个 JDK。

THE RELEASE FLAG release 标志

The —release flag was added through JEP 247. Before then, you could use -source and -target options. Those made sure that you didn’t use language features from the wrong level (-source) and that the generated bytecode conforms to the right Java release (-target). However, these flags did not enforce the right usage of APIs for the target JDK. When compiling with JDK 8, you could specify -source 1.7 -target 1.7 and still use Java 8 APIs in the code (though language features such as lambdas would be prohibited). Of course, the resulting bytecode doesn’t run on JDK 7, because it doesn’t ship the new Java 8 APIs. External tools such as Animal Sniffer had to be used to verify backward API compatibility. With —release, the right library level is enforced by the Java compiler as well—no need to install and manage multiple JDKs anymore.

—release 标志是通过 JEP 247(http://openjdk.java.net/jeps/247)添加的。在此之前,可以使用-source和-target选项。这些标志确保不会使用错误级别的语言特性(-source),并且生成的字节码符合正确的Java版本(-target)。但是,这些标志没有强制正确使用目标JDK的API。当使用JDK 8 进行编译时,可以指定-source 1.7-target 1.7,并在代码中仍使用 Java 8 API(尽管禁止使用 Lambda 表达式等语言功能)。当然,所生成的字节码不能在 JDK7 上运行,因为它不提供新的 Java 8 API。必须使用外部工具,如 AnimalsSniffer(http://www. mojhaus.org/animal-sniffer/)来验证后向 API 兼容性。如果使用—release,正确的库级别也由 Java 编译器强制执行——不再需要安装和管理多个 JDK。

10.6 Library Module Dependencies 库模块依赖关系

So far, we’ve assumed that the library to be migrated doesn’t have any dependencies beyond modules in the JDK. In practice, that’s not always the case. There are two main reasons a library has dependencies:

到目前为止,已经假定要迁移的库在 JDK 的模块之外没有任何依赖关系。实际上,情况通常并非如此。一个库存在依赖项通常有两个主要原因:

  1. The library consists of multiple, related JARs.
  2. External libraries are used by the library.

1)库由多个相关联的 API 组成。 2)库使用了外部库。

In the first case, there are dependencies between JARs within the library. In the second case, the library needs other external JARs. Both scenarios are addressed next.

在第一种情况下,库中的 JAR 之间存在依赖关系。在第二种情况下,库需要其他外部 JAR。接下来将讨论这两种情况。

10.6.1 Internal Dependencies 内部依赖关系

We’re going to explore the first scenario based on a library you’ve already seen in Chapter 8: Jackson. Jackson already consists of multiple JARs. The example was based on Jackson Databind, with two related Jackson JARs, as shown in Figure 10-1.

接下来,将根据第 8 章中所介绍的库 Jackson 来研究一下第一种情况。Jackson 由多个 JAR 组成。该示例基于 Jackson Databind 以及两个相关的 Jackson JAR,如图 10-1 所示。

Three related Jackson JARs

Turning those JARs into modules is the obvious thing to do, thereby preserving the current boundaries. Luckily, jdeps can also create several module descriptors at once for related JAR files (➥ chapter10/generate_module_descriptor_jackson):

将这些 JAR 转换为模块是明智之举,可以保留当前的边界。幸运的是,jdeps 还可以为相关的 JAR 文件(chapter10/generate_module_descriptor_jackson)同时创建多个模块描述符:

  1. jdeps --generate-module-info ./out *.jar

This results in three generated module descriptors:

上述代码生成三个模块描述符:

  1. module jackson.annotations {
  2. exports com.fasterxml.jackson.annotation;
  3. }
  4. module jackson.core {
  5. exports com.fasterxml.jackson.core;
  6. // Exports of all other packages omitted for brevity.
  7. provides com.fasterxml.jackson.core.JsonFactory with
  8. com.fasterxml.jackson.core.JsonFactory;
  9. }
  10. module jackson.databind {
  11. requires transitive jackson.annotations;
  12. requires transitive jackson.core;
  13. requires java.desktop;
  14. requires java.logging;
  15. requires transitive java.sql;
  16. requires transitive java.xml;
  17. exports com.fasterxml.jackson.databind;
  18. // Exports of all other packages omitted for brevity.
  19. provides com.fasterxml.jackson.core.ObjectCodec with
  20. com.fasterxml.jackson.databind.ObjectMapper;
  21. }

We can see in the last two module descriptors that jdeps also takes into account service providers. When the JAR contains service provider files (see “ServiceLoader Before Java 9” for more information on this mechanism), they are translated into provides .. with clauses.

可以在最后两个模块描述符中看到,jdeps 也考虑到了服务提供者。如果 JAR 包含服务提供者文件(请参阅“Java 9 之前的 ServiceLoader”内容,以了解关于此机制的更多信息),那么这些文件将被翻译成 provide … with 子句。

WARNING

Services uses clauses, on the other hand, cannot be automatically generated by jdeps. These must be added manually based on ServiceLoader usage in the library.

另一方面,服务 uses 子句不能由 jdeps 自动生成。这些子句必须根据库中的 ServiceLoader 使用情况手动添加。

The jacksons.databind descriptor requires the right platform modules based on the jdeps analysis. Furthermore, it requires the correct other Jackson library modules, whose descriptors are generated in the same run. Jackson’s latent structure automatically becomes explicit in the generated module descriptors. Of course, the hard task of demarcating the actual API of the modules is left to the Jackson maintainers. Leaving all packages exported is definitely not desirable.

jacksons.databind 描述符基于 jdeps 分析请求正确的平台模块。此外,它还需要其他 Jackson 库模块,其描述符在同一运行中生成。Jackson 的隐式结构在生成的模块描述符中自动变为显式。当然,标定模块实际 API 的艰巨任务则留给了 Jackson 维护者来完成。将所有的包导出是绝对不可取的。

Jackson is an example of a library that was already modular in structure, consisting of several JARs. Other libraries have made different choices. For example, Google Guava has chosen to bundle all its functionality in a single JAR. Guava aggregates many independently useful parts, ranging from alternative collection implementations to an event bus. However, using it is currently an all-or-nothing choice. The main reason cited by Guava maintainers for not modularizing the library is backward compatibility. Depending on Guava as a whole must be supported in future versions.

Jackson 是一个在结构上已经模块化的库示例,由几个 JAR 组成。而其他库做出了不同的选择。例如,Google Guava 选择将其所有功能捆绑到一个 JAR 中。Guava 将许多独立有用的部分聚合在一起,范围从可选集合实现到事件总线。但是,Guava 这么做完全是一种全无或全有的选择。Guava 维护者没有对该库进行模块化的主要原因是考虑到后向兼容性(https://github.com/google/guava/issues/605)。未来的版本必须支持Guava作为一个整体。

Creating an aggregator module that represents the whole library is one way to achieve this with the module system. In “Building a Facade over Modules”, we discussed this pattern in the abstract. For Guava, it might look like this:

创建一个表示整个库的聚合器模块是通过模块系统实现此功能的一种方法。在 5.4.1 节中已经简要地讨论了这个模式。对于 Guava,可能看起来如下所示:

  1. module com.google.guava {
  2. requires transitive com.google.guava.collections;
  3. requires transitive com.google.guava.eventbus;
  4. requires transitive com.google.guava.io;
  5. // .. and so on
  6. }

Each individual Guava module then exports the associated packages for that feature. Guava users can now require com.google.guava and transitively get all Guava modules, just as before. Implied readability ensures they can access all Guava types exported by the individual, smaller modules. Or, they can require only the individual modules necessary for the application. It’s the familiar trade-off between ease of use at development time or a smaller resolved dependency graph leading to a lower footprint at run-time.

然后,每个单独的 Guava 模块都会导出相关的包。与前面一样,Guava 用户可以请求 com.google.guava,并且以传递的方式获取所有的 Guava 模块。隐式可读性确保用户可以访问由单个小型模块导出的所有 Guava 类型。或者,可以仅请求应用程序所需的单个模块。这是在开发时的易用性和较小的解析依赖关系图(在运行时占用更少的空间)之间常见的折中方案。

NAMING THE GUAVA MODULE 命名 Guava 模块

In this hypothetical example, we arrived at the com.google.guava module name. Following our own advice from “Choosing a Library Module Name” (taking the longest common package prefix) would result in another module name: com.google.common. This is the name the Google Guava team has settled on at the time of writing. On the one hand, the link between the module name and its contained packages is clear with this name. On the other hand, maybe the package naming scheme wasn’t that great after all.

在这个假设的示例中,模块的名称为 com.google.guava。如果按照 10.2 节中所给出的建议(采用最长的公共包前缀),将会产生另一个模块名称:com. google.common。而该名称就是 Google Guava 团队在编写模块时已经确定的名字。一方面,使用该名称可以非常清楚地表明模块名称与所包含包之间的关系。另一方面,包命名方案毕竟不是那么好。

It’s awkward to have a module name for the Guava project that doesn’t include Guava. This illustrates, once again, that choosing a good name is hard. Discussions around commitment and package ownership are inevitable in this process.

如果 Guava 项目中的模块名称不包含 Guava,那将是非常尴尬的事情。这也再次说明,选择一个好名字是很困难的。在这个过程中,围绕着责任和包所有权展开讨论是不可避免的。

When your library consists of a single large JAR, consider splitting it up when modularizing. In many cases, the maintainability of the library increases, while at the same time users can be more specific about what APIs they want to depend on. Through an aggregator module, backward compatibility is offered for those who want to depend on everything in one go.

当库包含单个大型 JAR 时,可以考虑在模块化时将其拆分。在许多情况下,库的可维护性越高,用户就可以更具体地了解所依赖的 API。通过一个聚合器模块,可以为那些想要一劳永逸的人提供向后兼容性。

Especially if different independent parts of your API have different external dependencies, modularizing your library helps users. They can avoid unnecessary dependencies by requiring only the individual part of your API they need. Consequently, they’re not burdened by dependencies thrusted upon them through parts of the API they don’t use.

尤其是当 API 的不同独立部分有不同的外部依赖关系时,对库进行模块化可以更好地帮助用户。他们只需请求所需 API 的个别部分,以避免不必要的依赖项,因此不必受累于强加给他们的 API 中不使用部分的依赖项。

As a concrete example, recall the java.sql module and its dependency on java.xml (as discussed in “Implied Readability”). The only reason this dependency exists is the SQLXML interface. How many users of the java.sql module are using their database’s XML feature? Probably not that many.

作为一个具体的例子,回顾一下 java.sql 模块及其对 java.xml 的依赖关系(如 2.5 节所述)。存在该依赖关系的唯一原因是 SQLXML 接口。有多少 java.sql 模块的用户正在使用数据库的 XML 功能?可能并不是那么多。

Still, all consumers of java.sql now get java.xml “for free” in their resolved module graph. If java.sql were to be split up in java.sql and java.sql.xml, users would have a choice. The latter module then contains the SQLXML interface, doing a requires transitive java.xml (and java.sql). In that case, java.sql itself doesn’t need to require java.xml anymore. Users interested in the XML features can require java.sql.xml, whereas everyone else can require java.sql (without ending up with java.xml in the module graph).

不过,现在所有 java.sql 的使用者都可以在已解析的模块图中“免费”获取 java. xml。如果将 java.sql 拆分为 java.sql 和 java.sql.xml,则用户就可以进行选择。后一个模块包含 SQLXML 接口,执行 requires transitive java.xml(以及 java.sql)。此时,java.sql 本身不再需要 java.xml。对 XML 功能感兴趣的用户可以请求 java.sql.xml,而其他人可以请求 java.sql(不需要以模块图中的 java.xml 结束)。

Because this requires the breaking change of putting SQLXML in its own package (you can’t split a package over multiple modules), this was not an option for the JDK. This pattern is easier to apply to APIs that are already in different packages. If you can pull it off, segregating your modules based on their external dependencies can enormously benefit the users of your library.

因为上述模式需要将 SQLXML 放在自己的包中(不能将包拆分成多个模块),所以这不适用于 JDK。该模式更适用于已经在不同包中的 API。如果能把它脱离出来,那么根据外部依赖关系隔离模块可以极大地帮助库用户。

10.6.2 External Dependencies 外部依赖关系

Internal dependencies between libraries can be handled in module descriptors and are even taken care of by jdeps when generating preliminary module descriptors. What about dependencies on external libraries?

库之间的内部依赖关系可以在模块描述符中处理,甚至可以在生成初步模块描述符时由 jdeps 负责处理。那么对外部库的依赖关系应该如何处理呢?

TIP

Ideally, your library has zero external dependencies (frameworks are a different story altogether). Alas, we don’t live in an ideal world.

在理想情况下,库没有外部依赖关系(框架却完全不同)。唉,我们不是生活在一个理想的世界里。

If those external libraries are explicit Java modules, the answer is quite simple: a requires (transitive) clause in the module descriptor of the library suffices.

如果这些外部库是显式的 Java 模块,那么答案很简单:在库的模块描述符中添加 requires(transitive)子句就足够了。

What if the dependency is not modularized yet? It’s tempting to think there’s no problem, because any JAR can be used as an automatic module. While true, there’s a subtle issue related to naming, which we touched upon already in “Choosing a Library Module Name”. For the requires clause in the library’s module descriptor, we need a module name. However, the name of an automatic module depends on the JAR filename, which isn’t completely under our control. The true module name could change later, leading to module-resolution problems in applications using our library and the (now) modularized version of the external dependency.

如果依赖项没有模块化又该怎么办呢?很多人会认为这没有问题,因为任何 JAR 都可以用作自动模块。虽然这是真的,但还是存在一个与命名有关的细微问题,在 10.2 节中已经谈到了这个问题。对于库模块描述符中的 requires 子句,需要一个模块名称。但是,自动模块的名称取决于 JAR 文件名,而 JAR 文件名不完全在我们的控制之下。日后,真正的模块名称可能会发生变化,从而导致使用库和外部依赖项的(目前)模块化版本的应用程序中出现模块解析问题。

There is no foolproof solution to this issue. Adding a requires clause on an external dependency should be done only when you can be reasonably sure the module name is stable. One way to ensure this is to compel the maintainer of the external dependency to claim a module name with the Automatic-Module-Name header in the manifest. This, as you have seen, is a relatively small and low-risk change. Then, you can safely refer to the automatic module by this stable name. Alternatively, the external dependency can be asked to fully modularize, which is more work and harder to pull off. Any other approach potentially ends in failure due to an unstable module name.

这个问题没有万全的解决办法。只有当合理地确定模块名称是稳定的时候,才应该在外部依赖项上添加一个 requires 子句。确定模块名称是否稳定的方法之一是迫使外部依赖项的维护者使用清单中的 Automatic-Module-Name 头来声明模块名称。如你所见,这是一个相对较小且风险较低的变化。然后,可以使用这个稳定的名称安全地引用自动模块。或者,可以要求外部依赖项完全模块化,但这需要完成更多的工作。如果模块名称不稳定,那么其他方法都可能以失败告终。

WARNING

Maven Central discourages publishing modules referring to automatic modules that don’t have a stable name. Libraries should require only automatic modules that have an Automatic-Module-Name manifest entry.

Maven Central 不鼓励发布指向没有稳定名称的自动模块的模块。库只需要具有 Automatic-Module-Name 清单条目的自动模块。

One more trick is used by several libraries to manage external dependencies: dependency shading. The idea is to avoid external dependencies by inlining external code into the library. Simply put, the class files of the external dependency are copied into the library JAR. To prevent name clashes when the original external dependency would be on the classpath as well, the packages are renamed during the inlining process. For instance, classes from org.apache.commons.lang3 would be shaded into com.javamodularity.mylibrary.org.apache.commons.lang3. All this is automated and happens at build-time by post-processing bytecode. This prevents the atrociously long package names from seeping into actual source code. Shading is still a viable option with modules. However, it is recommended only for dependencies that are internal to the library. Exporting a shaded package, or exposing a shaded type in an exported API, is not recommended.

另一种技巧是使用多个库来管理外部依赖关系:依赖阴影(dependency shading)。其主要思想是通过将外部代码内联到库中来避免外部依赖。简而言之,外部依赖项的类文件被复制到库 JAR 中。为了防止原始外部依赖项也出现在类路径上时所发生的名称冲突,在内联过程中将重命名包。例如,来自 org.apache.commons.lang3 的类将会被重命名为 com.javamodularity.mylibrary.org.apache.commons.lang3。所有这些都是自动完成的,并通过后期处理字节码在构建时发生。这可以防止恶意软件包名称渗透到实际的源代码中。对于模块,阴影(shading)仍然是一个可行的选择。但是,建议仅用于库内部的依赖关系。不推荐导出阴影包,或者将已导出 API 中的阴影类型导出。

After these steps, we have both the internal and external dependencies of the library under control. At this point, our library is a module or collection of modules, targeting the minimum version of Java we want to support. But wouldn’t it be nice if the library implementation could use new Java features—while still being able to run it on our minimum supported Java version, that is?

完成上述步骤之后,就可以控制库的内部和外部依赖关系。此时,库是模块或模块集合,针对所需支持的最低版本的 Java。但是,如果库实现可以使用新的 Java 特性,同时仍然能够在最低支持的 Java 版本上运行,岂不是更好?

10.7 Targeting Multiple Java Versions 针对多个 Java 版本

One way to use new Java APIs in a library implementation without breaking backward compatibility is to use them optionally. Reflection can be used to locate new platform APIs if they are available, much like the scenario described in “Optional Dependencies”. Unfortunately, this leads to brittle and hard-to-maintain code. Also, this approach works only for using new platform APIs. Using new language features in the library implementation is still impossible. For example, using lambdas in a library while maintaining Java 7 compatibility is just not possible. The other alternative, maintaining and releasing multiple versions of the same library targeting different Java versions, is equally unattractive.

如果想要在不破坏向后兼容性的情况下在库实现中使用新的 Java API,一种方法是有选择性地使用它们。如 5.6 节所述,反射可用于定位新的平台 API(如果可用的话)。但不幸的是,这样做会导致代码变得脆弱且难以维护。而且,这种方法仅适用于使用新的平台 API。在库实现中使用新的语言功能仍然是不可能的。例如,在保持 Java 7 兼容性的情况下在库中使用 Lambda 表达式是不可能的。另一种方法是,维护和发布针对不同 Java 版本的同一个库的多个版本,该方法同样没有吸引力。

10.7.1 Multi-Release JARs 多版本 JAR

With Java 9, a new feature is introduced: multi-release JAR files. This feature allows different versions of the same class file to be packaged inside a single JAR. These different versions of the same class can be built against different major Java platform versions. At run-time, the JVM loads the most appropriate version of the class for the current environment.

如果使用 Java 9,则可以引入一个新功能:多版本 JAR 文件。此功能允许将同一个类文件的不同版本打包到单个 JAR 中。相同类的这些不同版本可以针对不同的主要 Java 平台版本进行构建。在运行时,JVM 会为当前环境加载最适合的类版本。

It’s important to note that this feature is independent of the module system, though it works nicely with modular JARs as well. With multi-release JARs, you can use current platform APIs and language features in your library the moment they are available. Users on older Java versions can still rely on the previous implementation from the same multi-release JAR.

需要注意的是,该功能是独立于模块系统的,尽管它与模块化 JAR 可以很好地一起工作。通过使用多版本 JAR,可以在库中使用当前平台的 API 和语言功能。而旧 Java 版本的用户仍然可以依靠同一个多版本 JAR 中以前的实现。

A JAR is multi-release enabled when it conforms to a specific layout and its manifest contains a Multi-Release: true entry. New versions of a class need to be in the META-INF/versions/<n> directory, where <n> corresponds to a major Java platform version. It’s not possible to version a class specifically for an intermediate minor or patch version.

JAR 在符合特定布局并且其清单包含 Multi-Release:true 条目时启用了多版本。类的新版本需要位于META-INF/versions/<n>目录中,其中<n>对应于主要的 Java 平台版本号。不能专门为次要版本或补丁版本进行版本升级。

WARNING

As with all manifest entries, no leading or trailing spaces must exist around the Multi-Release: true entry. The key and value of the entry are not case-sensitive.

与所有清单条目一样,Multi-Release:true 条目周围不能有前导或尾随空格。条目的关键字和值不区分大小写。

Here’s an example of the contents of a multi-release JAR (➥ chapter10/multirelease):

下面显示了多版本 JAR 的内容(chapter10/multirelease)

  1. mrlib.jar
  2. ├── META-INF
  3. ├── MANIFEST.MF
  4. └── versions
  5. └── 9
  6. └── mrlib
  7. └── Helper.class
  8. └── mrlib
  9. ├── Helper.class
  10. └── Main.class

It’s a simple JAR with two top-level class files. The Helper class also has an alternative version that uses Java 9 features under META-INF/versions/9. The fully qualified name is exactly the same. From the perspective of the library users, there’s only one released version of the library, represented by the JAR file. Internal use of the multi-release functionality should not violate that expectation. Therefore, all classes should have the exact same public signature in all versions. Note that the Java runtime does not check this, so the burden is on the developer and tooling to ensure that this is the case.

这是一个带有两个顶级类文件的简单 JAR。Helper 类还有一个使用 META-INF /versions/9 下 Java 9 功能的替代版本。完全限定名称是完全一样的。从库用户的角度来看,该库只有一个版本,由 JAR 文件表示。多版本功能的内部使用并不应违反用户期望。因此,所有类都应该在所有版本中具有完全相同的公共签名。请注意,Java 运行时并不对此进行检查,所以由开发人员和工具完成相关工作。

There are multiple valid reasons for creating a Java 9–specific version of Helper. To start, the original implementation of the class might use APIs that are removed or encapsulated in Java 9. The Java 9–specific Helper version can use replacement APIs introduced in Java 9 without disrupting the implementation used on earlier JDKs. Or the Java 9 version of Helper may use new features simply because they’re faster or better.

创建 Helper 类的 Java 9 特定版本有多种合理的原因。首先,类的原始实现可能使用了 Java 9 中已经删除或封装的 API。特定于 Java 9 的 Helper 版本可以使用 Java 9 中引入的替换 API,同时不用破坏早期 JDK 中所使用的实现。或者,Helper 类的 Java 9 版本可能会为了更快或更好地运行而使用新功能。

Because the alternative class file is under the META-INF directory, it is ignored by earlier JDKs. However, when running on JDK 9, this class file is loaded instead of the top-level Helper class. This mechanism works both on the classpath and on the module path. All classloaders in JDK 9 have been made multi-release-JAR aware. Because multi-release JARs are introduced with JDK 9, only 9 and up can be used under the versions directory. Any earlier JDKs will see only the top-level classes.

由于替代类文件位于 META-INF 目录下,因此早期的 JDK 将忽略它。但是,当在 JDK 9 上运行时,加载的是此类文件而不是顶级的 Helper 类。该机制在类路径和模块路径上都可以工作。JDK9 中的所有类加载器都可以进行多版本 JAR 识别。由于在 JDK 9 中引入了多版本 JAR,因此 versions 目录下只能使用 9 及以上版本。任何早期的 JDK 都只能看到顶级类。

You can easily create a multi-release JAR by compiling the different sources with different —release settings:

只需使用不同的—release 设置编译不同的源就可以创建一个多版本 JAR:

  1. javac --release 7 -d mrlib/7 src/<all top-level sources> 1
  2. javac --release 9 -d mrlib/9 src9/mrlib/Helper.java 2
  3. jar -cfe mrlib.jar src/META-INF/MANIFEST.MF -C mrlib/7 . 3
  4. jar -uf mrlib.jar --release 9 -C mrlib/9 . 4
  1. Compile all normal sources at the desired minimum release level.
  2. Compile the code only for Java 9 separately.
  3. Create a JAR file with the correct manifest and the top-level classes.
  4. Update the JAR file with the new —release flag, which places class files in the correct META-INF/versions/9 directory.

  1. 在所需的最低版本级别下编译所有常规源代码。
  2. 仅针对 Java 9 单独编译代码。
  3. 使用正确的清单和顶级类创建一个 JAR 文件。
  4. 使用新的—release 标志更新 JAR 文件,将类文件放置到正确的 META-INF/versions/9 目录中。

In this case, the specific Helper version for Java 9 comes from its own src9 directory. The resulting JAR works on Java 7 and up. Only when running on Java 9 is the specific Helper version compiled for Java 9 loaded.

在这种情况下,针对 Java 9 的特定 Helper 版本来自自己的 src9 目录。由此产生的 JAR 适用于 Java7 及更高版本。只有在 Java 9 上运行时,才会加载针对 Java 9 编译的特定 Helper 版本。

TIP

It’s a good idea to minimize the number of versioned classes. Factoring out the differences into a few classes decreases the maintenance burden. Versioning almost all classes in a JAR is undesirable.

尽量减少版本化类的数量是一个好主意。将差异分解成若干个类减少了维护的负担,但对 JAR 中的所有类进行版本化是不可取的。

After Java 10 is released, we can extend the mrlib library with a specific Helper implementation for that Java version:

在 Java 10 发布之后,可以使用针对该版本的特定 Helper 实现扩展 mrlib 库:

  1. mrlib.jar
  2. ├── META-INF
  3. └── versions
  4. ├── 10
  5. └── mrlib
  6. └── Helper.class
  7. └── 9
  8. └── mrlib
  9. └── Helper.class
  10. ├── mrlib
  11. ├── Helper.class
  12. └── Main.class

Running this multi-release JAR on Java 8 and below works the same as before, using the top-level classes. When running it on Java 9, the Helper from versions/9 is used. However, when running it on Java 10, the most specific match for Helper from versions/10 is loaded. The current JVM always loads the most recent version of a class, up to the version of the Java runtime itself. Resources abide by the same rules as classes. You can put specific resources for different JDK versions under versions, and they will be loaded in the same order of preference.

在 Java 8 及更低版本上运行这个多版本 JAR 与以前一样,使用顶级类。而在 Java 9 上运行时,则使用 versions/ 9 中的 Helper 类。同样,在 Java 10 上运行时,会加载与 versions/ 10 中的 Helper 最匹配的项。当前的 JVM 总是加载与 Java 运行时自身版本相匹配的该类的最新版本。资源遵守与类相同的规则。可以将不同 JDK 版本的特定资源放在 versions 目录中,并将按照相同的优先顺序加载它们。

Any class appearing under versions must also appear at the top level. However, it’s not required to have a specific implementation for each version. In the preceding example, it’s perfectly fine to leave out Helper under versions/9. Running the library on Java 9 then means it falls back to the top-level implementation, and the specific version is used only on Java 10 and later.

versions 目录下的任何类都必须出现在顶层,但并不要求每个版本都有特定的实现。在前面的示例中,把 Helper 放在 versions/ 9 下是完全正确的。在 Java 9 上运行库意味着它回退到顶层实现,并且特定版本仅在 Java 10 及更高版本上使用。

10.7.2 Modular Multi-Release JARs 模块化多版本 JAR

Multi-release JARs can be modular as well. Adding a module descriptor at the top level is enough. As discussed earlier, module-info.class will be ignored when using the JAR on pre-Java 9 runtimes. It’s also possible to put the module descriptor under versions/9 and up instead.

多版本 JAR 也可以是模块化的。只需在顶层添加模块描述符就可以了。如前所述,如果在 Java 9 之前的运行时上使用 JAR 时,module-info.class 将被忽略。也可以将模块描述符放在 versions/9 下。

That raises the question of whether it’s possible to have different versions of module-info.class. Indeed, it is allowed to have versioned module descriptors—for example one under versions/9 and one under versions/10. The allowed differences between module descriptors are minor. These differences should not lead to observable behavior differences across Java versions, much like the way different versions of normal classes must have the same signature.

这样做是否会引发具有不同版本的 module-info.class 的问题。的确,使用版本化的模块描述符是允许的,如 versions/9 下的模块描述符以及 versions/10 下的模块描述符。模块描述符之间所允许的差异应该尽可能小。这些差异不应导致 Java 版本之间可观察到的行为差异,就像普通类的不同版本必须具有相同的签名一样。

In practice, the following rules apply for versioned module descriptors:

在实践中,下列规则适用于版本化模块描述符:

  • Only nontransitive requires clauses on java.* and jdk.* modules may be different.
  • Service uses clauses may be different regardless of the service type.

  • 只有java.*jdk.*模块上非传递性的 requires 子句可以有所不同。
  • 无论服务类型如何,服务 uses 子句可能有所不同。

Neither the use of services nor the internal dependency on different platform modules leads to observable differences when the multi-release JAR is used on different JDKs. Any other changes to the module descriptor between versions are not allowed. If it is necessary to add (or remove) a requires transitive clause, the API of the module changes. This is beyond the scope of what multi-release JARs support. In that case, a new release of the whole library itself is in order.

当在不同的 JDK 上使用多版本 JAR 时,服务的使用以及不同平台模块的内部依赖关系都会导致可观察到的差异。不允许对版本之间的模块描述符进行任何其他更改。如果需要添加(或删除)requires transitive 子句,则模块的 API 将发生更改。这超出了多版本 JAR 支持的范围。在这种情况下,就生成了整个库的新版本。

If you are a library maintainer, you have your work cut out for you. Start by deciding on a module name and claiming it with Automatic-Module-Name. Now that your users can use your library as an automatic module, take the next step and truly modularize your library. Last, multi-release JARs lower the barrier for using Java 9 features in your library implementation while maintaining backward compatibility with earlier Java versions.

如果你是库的维护者,那么需要完成的工作大大减少。首先确定一个模块名称,并使用 Automatic-Module-Name 声明。用户可以将你的库作为自动模块来使用,并采取下一步措施对库进行真正的模块化。最后,多版本 JAR 既降低了在库实现中使用 Java 9 功能的障碍,同时又保持与早期 Java 版本的向后兼容性。