You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

280 lines
13 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 18 | 模块系统:怎么模块化你的应用程序?
你好我是范学雷。今天我们继续讨论Java平台模块系统Java Platform Module SystemJPMS
Java平台模块系统是在JDK 9正式发布的。在上一讲我们也说过这项重要的技术从萌芽到诞生花费了十多年的时间堪称Java出现以来最重要的新软件工程技术。
模块化可以帮助各级开发人员在构建、维护和演进软件系统时提高工作效率。更让人满意的是,它还非常简单、直观。我们不需要太长的学习时间就能快速掌握它。
这一节课我们就一起来看看应该怎么使用Java平台模块系统。
## 阅读案例
在前面的课程里我们多次使用了Digest这个案例来讨论问题。在这些案例里我们把实现的代码和接口定义的代码放在了同一个文件里。对于一次Java新特性的讨论来说这样做也许是合适的。我们可以使用简短的代码快速、直观地展示新特性。
```java
public sealed abstract class Digest {
private static final class SHA256 extends Digest {
// snipped, implementation code.
}
private static final class SHA512 extends Digest {
// snipped, implementation code.
}
public static Returned<Digest> of(String algorithm) {
// snipped, implementation code.
}
public abstract byte[] digest(byte[] message);
}
```
但是如果放到生产环境这样的示例就不一定是一个好的导向了。因为Digest的算法可能有数十种。其中有老旧废弃的算法有即将退役的算法还有当前推荐的算法。把这些算法的实现都装到一个瓶子里似乎有点拥挤。
而且不同的算法可能有不同的许可证和专利限制实现的代码也可能是由不同的个人或者公司提供的。同一个算法可能还会有不同的实现有的实现需要硬件加速有的实现需要使用纯Java代码。这些情况下这些实现代码其实都是没有办法装到同一个瓶子里的。
所以,典型的做法就是分离接口和实现。
首先,我们来看一看接口的设计。下面的代码就是一个接口定义的例子。
```java
package co.ivi.jus.crypto;
import java.util.ServiceLoader;
public interface Digest {
byte[] digest(byte[] message);
static Returned<Digest> of(String algorithm) {
ServiceLoader<DigestManager> serviceLoader =
ServiceLoader.load(DigestManager.class);
for (DigestManager cryptoManager : serviceLoader) {
Returned<Digest> rt = cryptoManager.create(algorithm);
switch (rt) {
case Returned.ReturnValue rv -> {
return rv;
}
case Returned.ErrorCode ec -> {
continue;
}
}
}
return Returned.UNDEFINED;
}
}
```
在这个例子里我们只定义了Digest的公开接口以及实现获取的方法使用ServiceLoader而没有实现具体算法的代码。同时呢我们希望Digest接口所在的包也是公开的这样应用程序可以方便地访问这个接口。
有了Digest的公开接口我们还需要定义连接公开接口和私有实现的桥梁也就是实现的获取和供给办法。下面这段代码定义的就是这个公开接口和私有实现之间的桥梁。Digest公开接口的实现代码需要访问这个桥梁接口所以它也是公开的接口。
```java
package co.ivi.jus.crypto;
public interface DigestManager {
Returned<Digest> create(String algorithm);
}
```
然后我们来看看Digest接口实现的部分。有了Digest的公开接口和实现的桥梁接口Digest的实现代码就可以放置在另外一个Java包里了。比如下面的例子里我们把Sha256的实现放在了co.ivi.jus.impl这个包里。
```plain
package co.ivi.jus.impl;
import co.ivi.jus.crypto.Digest;
import co.ivi.jus.crypto.Returned;
final class Sha256 implements Digest {
static final Returned.ReturnValue<Digest> returnedSha256;
// snipped
private Sha256() {
// snipped
}
@Override
public byte[] digest(byte[] message) {
// snipped
}
}
```
因为这只是一个算法的实现代码我们不希望应用程序直接调用实现的子类也不希望应用程序直接访问这个Java包。所以Sha256这个子类使用了缺省的访问修饰符。
同时在这个Java包里我们也实现了Sha256的间接获取方式也就是实现了桥梁接口。
```java
package co.ivi.jus.impl;
// snipped
public final class DigestManagerImpl implements DigestManager {
@Override
public Returned<Digest> create(String algorithm) {
return switch (algorithm) {
case "SHA-256" -> Sha256.returnedSha256;
case "SHA-512" -> Sha512.returnedSha512;
default -> Returned.UNDEFINED;
};
}
}
```
稍微有点遗憾的是由于ServiceLoader需要使用public修饰的桥梁接口所以我们不能使用除了public以外的访问修饰符。也就是说如果应用程序加载了这个Java包它就可以直接使用DigestManagerImpl类。这当然不是我们期望的使用办法。
我们并不希望应用程序直接使用DigestManagerImpl类然而JDK 8之前的Java世界里我们并没有简单有效的、强制性的封装办法。所以我们的解决办法通常是对外宣称“co.ivi.jus.impl”这个包是一个内部Java包请不要直接使用。这需要应用程序的开发者仔细地阅读文档分辨内部包和公开包。
但在Java 9之后的Java世界里我们就可以使用Java模块来限制应用程序使用DigestManagerImpl类了。
## 使用Java模块
下面我们来一起看看Java模块是怎么实现这样的限制的。
### 模块化公开接口
首先呢我们把公开接口的部分也就是co.ivi.jus.crypto这个Java包封装到一个Java模块里。我们给这个模块命名为jus.crypto。Java模块的定义使用的是module-info.java这个文件。这个文件要放在源代码的根目录下。下面的代码就是我们封装公开接口的部分的module-info.java文件。
```java
module jus.crypto {
exports co.ivi.jus.crypto;
uses co.ivi.jus.crypto.DigestManager;
}
```
第一行代码里的“module”就是模块化定义的关键字。紧接着module的就是要定义的模块的名字。在这个例子里我们定义的是jus.crypto这个Java模块。
第二行代码里的“exports”, 说明了这个模块允许外部访问的API也就是这个模块的公开接口。“模块的公开接口”是一个Java模块带来的新概念。
没有Java模块的时候除了内部接口我们可以把public访问修饰符修饰的外部接口看作是公开的接口。这样的规则需要我们去人工分辨内部接口和外部接口。
但有了Java模块之后我们就知道使用了“exports”模块定义、并且使用了public访问修饰符修饰的接口就是公开接口。这样公开接口就有了清晰的定义我们就不用再去人工分辨内部接口和外部接口了。
而第四行代码里的“uses”呢则说明这个模块直接使用了DigestManager定义的服务接口。
你看这么简短的五行代码就把co.ivi.jus.crypto这个Java模块化了。它定义了公开接口以及要使用的服务接口。
### 模块化内部接口
然后呢我们要把内部接口的部分也就是co.ivi.jus.impl这个Java包也封装到一个Java模块里。下面的代码就是我们封装内部接口部分的module-info.java文件。
```java
module jus.crypto.impl {
requires jus.crypto;
provides co.ivi.jus.crypto.DigestManager with co.ivi.jus.impl.DigestManagerImpl;
}
```
在这里第一行代码定义了jus.crypto.impl这个Java模块。
第二行代码里的“requires”说明这个模块需要使用jus.crypto这个模块。也就是说定义了这个模块的依赖关系。有了这个明确定义的依赖关系加载这个模块的时候Java运行时就不再需要地毯式地搜索依赖关系了。
第四行代码里的“provides”说明这个模块实现了DigestManager定义的服务接口。同样的有了这个明确的定义服务接口实现的搜索也不需要地毯式地排查了。
需要注意的是这个模块并没有使用“exports”定义模块的公开接口。这也就意味着虽然在co.ivi.jus.impl这个Java包里有使用public访问修饰符修饰的接口它们也不能被模块外部的应用程序访问。这样我们就不用担心应用程序直接访问DigestManagerImpl类了。取而代之的应用程序只能通过DigestManager这个公开的接口间接地访问这个实现类。这是我们想要的封装效果。
### 模块化应用程序
有了公开接口和实现我们再来看看该怎么模块化应用程序。下面的代码是我们使用了Digest公开接口的一个小应用程序。
```java
package co.ivi.jus.use;
import co.ivi.jus.crypto.Digest;
import co.ivi.jus.crypto.Returned;
public class UseCase {
public static void main(String[] args) {
Returned<Digest> rt = Digest.of("SHA-256");
switch (rt) {
case Returned.ReturnValue rv -> {
Digest d = (Digest) rv.returnValue();
d.digest("Hello, world!".getBytes());
}
case Returned.ErrorCode ec ->
System.getLogger("co.ivi.jus.stack.union")
.log(System.Logger.Level.INFO,
"Failed to get instance of SHA-256");
}
}
}
```
下面的代码就是我们封装这个应用程序的module-info.java文件。
```java
module jus.crypto.use {
requires jus.crypto;
}
```
在这里第一行代码定义了jus.crypto.use这个Java模块。
第二行代码里的“requires”, 说明这个模块需要使用jus.crypto这个模块。
需要注意的是这个模块并没有使用“exports”定义模块的公开接口。那么我们该怎么运行UseCase这个类的main方法呢其实和传统的Java代码相比模块的编译和运行有着自己的特色。
### 模块的编译和运行
在javac和java命令行里我们可以使用“module-path”指定java模块的搜索路径。在Jar命令行里我们可以使用“main-class”指定这个Jar文件的main函数所在的类。在Java命令里我们可以使用“module”指定main函数所在的模块。
有了这些选项的配合在上面的例子里我们就不需要把UseCase在模块里定义成公开类了。我们来看看这些选项是怎么使用的。
```bash
$ cd jus.crypto
$ javac --enable-preview --release 17 \
-d classes src/main/java/co/ivi/jus/crypto/* \
src/main/java/module-info.java
$ jar --create --file ../jars/jus.crypto.jar -C classes .
$ cd ../jus.crypto.impl
$ javac --enable-preview --release 17 \
--module-path ../jars -d classes \
src/main/java/co/ivi/jus/impl/* \
src/main/java/module-info.java
$ jar --create --file ../jars/jus.crypto.impl.jar -C classes .
$ cd ../jus.crypto.use
$ javac --enable-preview --release 17 \
--module-path ../jars -d classes \
src/main/java/co/ivi/jus/use/* \
src/main/java/module-info.java 
$ jar --create --file ../jars/jus.crypto.use.jar \
--main-class co.ivi.jus.use.UseCase \
-C classes .
$ java --enable-preview --module-path ../jars --module jus.crypto.use
```
我在专栏里不会讲解这些选项的细节。具体的用法,我更希望你去找第一手的资料。下面的这个[备忘单](https://www.jrebel.com/system/files/java-9-modules-cheat-sheet.pdf)是我看到的一个比较好的总结。你可以打印下来备用,用熟了之后再丢掉。
![图片](https://static001.geekbang.org/resource/image/03/df/03f55f79f08dcfc29f3ea6f80dcea2df.jpg?wh=1920x1358)
## 总结
到这里我来做个小结。前面我们讨论了怎么使用Java模块封装我们的代码了解了module-info.java文件以及它的结构和关键字。
总体来看Java模块的使用是简单、直观的。Java模块的使用实现了更好的封装也定义了模块和Java包之间的依赖关系。有了依赖关系Java语言就能够实现更快的类检索和类加载了。这样的性能提升通过模块化就能实现还不需要更改代码。
如果面试的时候讨论到了Java平台模块系统你可以聊一聊Java模块封装的关键字以及这些关键字能够起到的作用。我相信这是一个有意思、有深度的话题。
## 思考题
在前面的讨论中我们把DigestManager定义成了公开接口。我们希望Digest的实现可以使用这个桥梁接口但是我们又不希望应用程序直接使用它。取而代之的应用程序应该使用Digest.of方法获得算法的实现。从这个意义上说我们前面的案例并没有做好封装。
那么有没有更好的办法把DigestManager也封装起来让应用程序无法调用呢这是我们这一次、也是最后一次的思考题。
欢迎你在留言区留言、讨论,分享你的阅读体验以及你的改进。
注:本文使用的完整代码可以从[GitHub](https://github.com/XueleiFan/java-up)下载。