上篇文章介绍了什么是模块化,以及Java模块化解决的问题。本文将介绍Java模块化的相关概念及具体写法。本文将从如下两个方面介绍模块化:
- 模块描述符
- 服务
1. 模块描述符
为了体现模块之间的关系,必须定义全新的模块描述文件,类似于Maven中的pom文件,Java9称之为模块描述符。
模块描述符是一个固定名称的java文件,所有的模块描述符文件名称固定位 module-info.java,其内部存在特定的结构。
下面是两个示例,其中easytext.cli
模块依赖easytext.analysis
模块。
module easytext.analysis {
exports javamodularity.easytext.analysis;
}
module easytext.cli {
requires easytext.analysis;
}
上面示例中一共有三个新增的关键字,这三个关键字也是模块化系统中最常用的关键字。
- module: 用来定义一个模块,后面紧跟模块名称,在同一个模块路径下,模块名称不允许相同。
- exports: 用来指定开放那个包作为API供外部调用,没有开放的api不允许被调用。
- requires: 用来执行依赖的模块,依赖必须显示指定。
通过exports和requires的配合使用,达到模块化的目的,即强封装、显式依赖。
为了描述这种依赖关系,Java9新增了一个概念,名为可读性(readability)
,easytext.cli
模块依赖easytext.analysis
模块,也可以说 easytext.cli
模块可以读取easytext.analysis
模块。
模块路径(module path) 和 类路径(class path)
类路径(class path)是指Java编译和运行的基础目录,类路径下所有文件都是平行的,不存在层级关系,一旦服务启动,所有类都可能会被加载。
模块路(module path)径是指存放Java模块的目录,并不是模块路径下的所有模块都会被加载,其以根模块(root module)为起点,使用模块描述符递归查找依赖的模块,它存在层级关系,是一个树状结构。
可读性(readability) 和 可访问性(accessible)
可访问性(accessible)是指public、protected、(default)、private这四个级别,它们作用在类与类之间,提供访问控制。
可读性(readability)则提供模块与模块之间的访问控制,是对可访问性的功能补充。
可读性(readability)不止作用于运行阶段,与可访问性一样也作用于编译阶段,即未明确指定依赖的模块,编译将直接报错。
隐式可读性
有时候简单的依赖关系不能满足一些特定场景,比如依赖传递特性。模块化系统通过transitive
关系字,解决依赖的传递问题,这也称为隐式可读性
。
如下所示,java.se
模块中隐式依赖了一些模块,当其它模块依赖java.se
时,会自动依赖这些模块。通过隐式可读性,我们可以聚合不同的模块,为模块分组,java.se
就是一个最常用的模块组合。
module java.se {
requires transitive java.desktop;
requires transitive java.sql;
requires transitive java.xml;
// 省略
}
限制导出
在某些情况下,可能只需要将包暴露给特定的某些模块。此时,可以在模块描述符中使用限制导出。可以在java.xml模块中找到限制导出的示例:
module java.xml{
...
exports com.sun.xml.internal.stream.writers to java.xml.ws
...
}
一般来说,应该避免在模块之间使用限制导出。使用限制导出意味着在导出模块和允许的使用者之间建立了直接的联系。该特性最大的目的是为了处理历史问题,如对旧版本JDK模块化。
2. 服务(Service)
应用程序模块化后,被exports的包,通常仅包含接口,不包含具体实现,且实现类的个数可能是一个或多个。
为了充分解耦,接口的实现类不能由客户端创建,仅能由服务端提供,即Analyzer analyzer = ???
。对于这种情况,工厂模式是一个方案。
public class AnalyzerFactory {
public static List<String> getSupportedAnalyses() {
return List.of(FleschKincaid.NAME, Coleman.NAME);
}
public static Analyzer getAnalyzer(String name) {
switch (name) {
case FleschKincaid.NAME: return new FleschKincaid();
case Coleman.NAME: return new Coleman();
default: throw new IllegalArgumentException("No such analyzer!");
}
}
}
通过引入工厂模式,客户端只需获得服务名称列表后,根据名称选择使用哪个服务即可,而无需知道具体的实现类。
当我们想为Analyzer
增加一个实现类时,必须要修改AnalyzerFactory
才能实现,这违背了开闭原则。同时AnalyzerFactory
自身也必须依赖所有的实现,这样exports的包中也包含了实现类。
为了解决这个问题,模块化系统提供了服务(Service)的功能。通过使用ServiceLoader API
可在模块描述符和代码中表示服务。
服务提供者的模块描述符如下,通过provides with
关键字,确定了接口的实现类。可以通过ServiceLoader.load
的方式获得所有的Analyzer
实现。
module easytext.analysis.coleman {
requires easytext.analysis.api;
provides javamodularity.easytext.analysis.api.Analyzer
with
javamodularity.easytext.analysis.coleman.ColemanAnalyzer;
}
public interface Analyzer {
String getName();
double analyze(List<List<String>> text);
static Iterable<Analyzer> getAnalyzers() {
return ServiceLoader.load(Analyzer.class);
}
}
客户端的模块描述符如下,通过uses
关键字,指定想要使用的接口。
module easytext.cli {
requires easytext.analysis.api;
uses javamodularity.easytext.analysis.api.Analyzer;
}
Iterable<Analyzer> analyzers = Analyzer.getAnalyzers();
如果存在多个提供者,但只对“最好的”实现感兴趣,该怎么做呢?Java模块系统不可能知道哪个实现最合适,所以只能由应用程序自己决定。
服务绑定的模块解析
服务的
provides with
为解析过程添加了另一个维度。模块路径中使用provides with
关键字的模块,将被自动加载,而无需再使用requires
显示声明。
服务(Service)是可选的
与
requires
和exports
不同,服务(Service)是可选的,只有在需要的时候采用即可。
3. 小结
本文介绍了Java模块化的基本使用方式,过程中涉及的关键字汇总如下。
关键字 | 描述 |
---|---|
module |
声明模块名称,在模块路径下,模块名称必须是唯一的。 |
requires |
声明依赖的模块,只有模块被依赖后才能正常使用。 |
transitive |
跟随requires 使用,时依赖可以传递。 |
exports |
声明当前模块开放的package,只有被开放的package才能正常使用。 |
uses |
声明要使用的服务,该关键字没有requires 的功能,必须单独声明requires 指定uses 接口的所在的模块。 |
provides with |
声明服务的提供者,模块解析过程中,将自动解析服务提供者所在的模块,类似服务提供者被requires 了。 |
参考的文章
Java 9模块化开发:核心原则与实践 (O’Reilly精品图书系列)
java9-modularity/examples