说明
Java SE 9
引入了模块系统,模块就是代码和数据的封装体。模块的代码被组织成多个包,每个包中包含Java类和接口;模块的数据则包括资源文件和其他静态信息。
在 module-info.java
文件中,我们可以用新的关键词 module
来声明一个模块。
Java 9将JDK划分为多个模块以支持各种配置。参考 JEP 200: The Modular JDK
可以使用如下命令查询所有的模块(注:JDK 版本大于等于9)
java --list-modules
模块描述符是在名为 module-info.java
的文件中定义的模块声明的编译版本。每个模块声明都以关键字 module
开头,后跟唯一的模块名称和括在大括号中的模块正文,格式如下
module modulename { }
模块声明的主体可以是空的,也可以包含各种模块指令(比如: exports
, module
, open
, opens
, provides
, requires
, uses
, with
等)。
下面介绍各种指令的含义
require 指令指定此模块依赖于另一个模块,这种关系称为模块依赖项。每个模块必须显式声明其依赖项。当模块A需要模块B时,模块A称为读取模块B,模块B由模块A读取。要指定对另一个模块的依赖关系,请使用 require,格式如下
requires modulename;
require static 指令,用于指示在编译时需要模块,但在运行时是可选的。这称为可选依赖项。
requires transitive 表示模块的隐含可读性,
如果这样写
module a { requires b; }
表示
a模块强制要求存在b模块,这称为可靠配置。
a模块可以读入b模块的文件,这称为可读性。
a模块可以访问b模块的代码,这称为可访问性。
但是如果写成
module a { requires transitive b; }
表示:
如果其他模块比如c依赖于a模块,a模块使用 requires transitive
引用b模块,那么c模块即便没有直接声明依赖于b模块,它对b模块也是具有可读性的,这个称之为隐含可读性。
即: a requires transitive b
加上 c requires a
,则c对b也是可读。如果去掉 transitive
,则c对b不可读。
exports 指令指定模块的一个包,其 public
类型(及其嵌套的 public
类型和 protected
类型)应可供所有其他模块中的代码访问。
exports...to 指令使您能够在逗号分隔的列表中精确指定哪些模块或模块的代码可以访问导出的包,这称为限定导出。
uses 指令指定此模块使用的服务,使该模块成为服务使用者。服务是实现接口或扩展 uses
指令中指定的抽象类的类的对象。
provides...with 指令指定模块提供服务实现,使模块成为服务提供者。指令的 provide 部分指定列出的接口或抽象类,而 with 部分指令指定实现接口或扩展抽象类的服务提供程序类的名称。
open , opens, 和 opens...to 指令用于了解包中的所有类型以及某个类型的所有成员(甚至是其私有成员),无论您是否允许此功能。在 JDK 9 之前,这一功能是通过反射来实现的。
opens 指令
opens package
允许仅运行时访问包(package)。
opens...to 指令
opens package to comma-separated-list-of-modules
允许特定模块(module)仅运行时访问包(package)。
open 指令
open module modulename { // module directives }
允许仅运行时访问模块中的所有包。
环境准备
本地的 JDK 版本应该大于等于 JDK 9。
使用示例
本文的示例均参考于: Project Jigsaw: Module System Quick-Start Guide
注:以下示例均在Windows下进行,在Mac或者Linux下把\\换成/。javac的版本一定要大于等于JDK 9。
示例1
新建一个 example-01
文件夹,作为项目根目录,在项目根目录下创建一个 src
文件夹,在这个文件夹下,创建一个文件夹作为 module
,文件夹名称为: com.greetings
。在 com.greetings
文件夹下新建 module-info.java
文件夹,同时,在 com.greetings
文件夹中新建包,
即在 com.greetings
文件夹下新建 com
文件夹,在 com
文件夹下新建 greetings
文件夹。在 greetings
文件夹下新建 Main.java
。
目录结构如下
- example-01 - src - com.greetings - module-info.java - com - greetings - Main.java
module-info.java
文件中内容如下
module com.greetings { }
Main.java
中内容如下
package com.greetings; public class Main { public static void main(String[] args) { System.out.println("Greetings!"); } }
在 example-01
目录下新建 mods
文件夹,用于存放编译后的目录,在 mods
文件夹中新建一个 com.greetings
的目录,
目录结构变成了如下
- example-01 - mods - com.greetings - src - com.greetings - module-info.java - com - greetings - Main.java
在 example-01
目录下执行如下编译命令,
javac -d mods\\com.greetings src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java
编译完毕后,项目目录如下
- example-01 - mods - com.greetings - module-info.class - com - greetings - Main.class - src - com.greetings - module-info.java - com - greetings - Main.java
接下来运行 java
命令进行执行
java --module-path mods -m com.greetings/com.greetings.Main
控制台输出结果
Greetings!
--module-path
是模块路径,其值是一个或多个包含模块的目录。 -m
选项指定的是主模块,斜线后的值是模块中主类的类全名。
示例2
如上例,我们准备好文件夹目录和相关文件
- example-02 - mods - com.greetings - org.astro - src - com.greetings - module-info.java - com - greetings - Main.java - org.astro - module-info.java - org - astro - World.java
其中 src\\org.astro\\module-info.java
内容如下
module org.astro { exports org.astro; }
src\\org.astro\\org\\astro\\World.java
内容如下
package org.astro; public class World { public static String name() { return "world"; } }
src\\com.greetings\\module-info.java
module com.greetings { requires org.astro; }
`src\com.greetings\com\greetings\Main.java
package com.greetings; import org.astro.World; public class Main { public static void main(String[] args) { System.out.format("Greetings %s!%n", World.name()); } }
然后,在 example-02
下分别对 org.astro
和 com.greetings
两个模块进行编译。
javac -d mods\\org.astro src\\org.astro\\module-info.java src\\org.astro\\org\\astro\\World.java
javac --module-path mods -d mods\\com.greetings src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java
编译完成后,项目目录如下
- example-02 - mods - com.greetings - module-info.class - com - greetings - Main.class - org.astro - module-info.class - org - astro - World.class - src - com.greetings - module-info.java - com - greetings - Main.java - org.astro - module-info.java - org - astro - World.java
然后运行
java --module-path mods -m com.greetings/com.greetings.Main
控制台打印
Greetings world!
示例3
本示例演示如何打包,新建 example-03
文件夹,把示例2的所有代码和目录都原封不动拷贝进去。
- example-03 - mods - com.greetings - org.astro - src - com.greetings - module-info.java - com - greetings - Main.java - org.astro - module-info.java - org - astro - World.java
在 example-03
文件夹中新建一个 mlib
的文件夹,用于存放打包后的文件。
- example-03 - mlib - mods - com.greetings - org.astro - src - com.greetings - module-info.java - com - greetings - Main.java - org.astro - module-info.java - org - astro - World.java
执行打包命令,根据依赖关系,应该先打 org.astro
的包。
jar --create --file=mlib\\org.astro@1.0.jar --module-version=1.0 -C mods\\org.astro .
然后再打 com.greetings
的包
jar --create --file=mlib\\com.greetings.jar --main-class=com.greetings.Main -C mods\\com.greetings .
打包完成后,目录结构如下
- example-03 - mlib - com.greetings.jar - org.astro@1.0.jar - mods - com.greetings - org.astro - src - com.greetings - module-info.java - com - greetings - Main.java - org.astro - module-info.java - org - astro - World.java
接下来执行 jar
包
java -p mlib -m com.greetings
控制台打印
Greetings world!
jar
工具有许多新的选项(见jar -help),其中之一是打印作为模块化JAR打包的模块的模块声明。
jar --describe-module --file=mlib/org.astro@1.0.jar
控制台输出
org.astro@1.0 jar:file:///C:/workspace/hello-module/example-03/mlib/org.astro@1.0.jar!/module-info.class exports org.astro requires java.base mandated
示例4
服务允许服务消费者模块和服务提供者模块之间进行松散耦合。
这个例子有一个服务消费者模块和一个服务提供者模块。
模块 com.socket
输出了一个网络套接字的 API
。该 API
在包 com.socket
中,所以这个包被导出了。该 API
是可插拔的,允许替代的实现。服务类型是同一模块中的 com.socket.spi.NetworkSocketProvider
类,因此包 com.socket.spi
也是导出的。
模块 org.fastsocket
是一个服务提供者模块。它提供了 com.socket.spi.NetworkSocketProvider
的一个实现。它没有输出任何包。
创建 example-04
文件夹和相关目录
- example-04 - mods - com.socket - com.greetings - org.fastsocket - src - com.socket - module-info.java - com - socket - spi - NetworkSocketProvider.java - NetworkSocket.java - com.greetings - module-info.java - com - greetings - Main.java - org.fastsocket - module-info.java - org - fastsocket - FastNetworkSocketProvider - FastNetworkSocket.java
其中 src\\com.socket\\module-info.java
module com.socket { exports com.socket; exports com.socket.spi; uses com.socket.spi.NetworkSocketProvider; }
src\\com.socket\\com\\socket\\NetworkSocket.java
代码如下
package com.socket; import java.io.Closeable; import java.util.Iterator; import java.util.ServiceLoader; import com.socket.spi.NetworkSocketProvider; public abstract class NetworkSocket implements Closeable { protected NetworkSocket() { } public static NetworkSocket open() { ServiceLoader<NetworkSocketProvider> sl = ServiceLoader.load(NetworkSocketProvider.class); Iterator<NetworkSocketProvider> iter = sl.iterator(); if (!iter.hasNext()) throw new RuntimeException("No service providers found!"); NetworkSocketProvider provider = iter.next(); return provider.openNetworkSocket(); } }
src\\com.socket\\com\\socket\\spi\\NetworkSocketProvider.java
代码如下
package com.socket.spi; import com.socket.NetworkSocket; public abstract class NetworkSocketProvider { protected NetworkSocketProvider() { } public abstract NetworkSocket openNetworkSocket(); }
src\\org.fastsocket\\module-info.java
module org.fastsocket { requires com.socket; provides com.socket.spi.NetworkSocketProvider with org.fastsocket.FastNetworkSocketProvider; }
src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocketProvider.java
代码如下
package org.fastsocket; import com.socket.NetworkSocket; import com.socket.spi.NetworkSocketProvider; public class FastNetworkSocketProvider extends NetworkSocketProvider { public FastNetworkSocketProvider() { } @Override public NetworkSocket openNetworkSocket() { return new FastNetworkSocket(); } }
src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocket.java
代码如下
package org.fastsocket; import com.socket.NetworkSocket; class FastNetworkSocket extends NetworkSocket { FastNetworkSocket() { } public void close() { } }
src/com.greetings/module-info.java
代码如下
module com.greetings { requires com.socket; }
src\\com.greetings/com\\greetings\\Main.java
代码如下
package com.greetings; import com.socket.NetworkSocket; public class Main { public static void main(String[] args) { NetworkSocket s = NetworkSocket.open(); System.out.println(s.getClass()); } }
接下来是编译,根据依赖顺序
先编译 com.socket
javac -d mods --module-source-path src src\\com.socket\\module-info.java src\\com.socket\\com\\socket\\NetworkSocket.java src\\com.socket\\com\\socket\\spi\\NetworkSocketProvider.java
再编译 org.fastsocket
javac -d mods --module-source-path src src\\org.fastsocket\\module-info.java src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocket.java src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocketProvider.java
最后编译 com.greetings
javac -d mods --module-source-path src src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java
最后执行
java -p mods -m com.greetings/com.greetings.Main
控制台打印出结果
class org.fastsocket.FastNetworkSocket
以上输出确认了服务提供者已经被定位,并且它被用作 NetworkSocket
的工厂。
示例5
jlink
是链接器工具,可以用来链接一组模块,以及它们的横向依赖关系,以创建一个自定义的模块化运行时镜像(见 JEP 220 )。
该工具目前要求模块路径上的模块以模块化的 JAR
或 JMOD
格式打包。 JDK
构建以 JMOD
格式打包标准模块和 JDK
专用模块。
下面的例子创建了一个运行时镜像,其中包含com.greetings模块和它的横向依赖:
jlink --module-path $JAVA_HOME/jmods:mlib --add-modules com.greetings --output greetingsapp
--module-path
的值是一个包含打包模块的目录 PATH
。在 Microsoft Windows
上,将路径分隔符':'替换为';'。
$JAVA_HOME/jmods
是包含 java.base.jmod
及其他标准和 JDK
模块的目录。
模块路径上的 mlib
目录包含 com.greetings
模块的工件。
jlink
工具支持许多高级选项来定制生成的图像,更多选项见 jlink --help
示例6
--patch-module
从Doug Lea的CVS中签出 java.util.concurrent
类的开发者将习惯于用 -Xbootclasspath/p
来编译源文件和部署这些类。
-Xbootclasspath/p
已经被删除,它的模块替代品是选项 --patch-module
,用于覆盖模块中的类。它也可以被用来增加模块的内容。 javac
也支持 --patch-module
选项,可以像模块的一部分一样编译代码。
下面是一个编译新版本的 java.util.concurrent.ConcurrentHashMap
并在运行时使用它的例子:
javac --patch-module java.base=src -d mypatches/java.base \ src/java.base/java/util/concurrent/ConcurrentHashMap.java
java --patch-module java.base=mypatches/java.base ...
代码
参考资料
Project Jigsaw: Module System Quick-Start Guide
Java 9 Modules - Developing Java 9 Modules with Apache Maven