具名模块

先言,java模块化是java9之后才引入的,java8之前,java程序都是非模块化的。
java9将java中的基础类库,比如java.basejava.logging等,都单独拆分打包成模块。可以通过命令

$

查看所有的模块。所有的.jmod文件,存放在$JAVA_HOME/jmods目录下。

1.创建模块

使用idea创建一个普通的java工程module-A,创建好的项目如下:

在src下新建module_info.java, 添加如下代码:

module_info.java
1
2
3
4
mavaodule module.A {
// 依赖java.logging模块
requires java.logging;
}

修改Main.java

Main.java
1
2
3
4
5
6
7
8
import java.util.logging.Logger;

public class Main {
public static void main(String[] args) {
Logger log = Logger.getLogger("Main");
log.info("Hello World!");
}
}

运行Main.main方法,启动失败:

报错信息说的是无效的模块描述异常,Main.class在顶层目录下,而不在包下。那么我们在src下新建一个package叫做top,将Main.java移动到top包下,重新运行Main.main方法,成功运行。

这样,我们通过给普通java工程添加module_info.java,就可以将java工程变成模块化项目。

2.打包模块

在我们运行Main.main方法之后,按照idea自动配置,输出class文件到out/production/module-A目录下,下一步,我们需要把class文件打包成jar包。
这里借助idea配置artifact,构建出jar包。

idea配置
idea配置

配置好之后,点击Build Artifacts,选择module-A,点击Build,即可生成jar包。

out目录结构
out目录结构

目前,生成的jar包还不能称之为模块。我们还需要一个工具jmod来打包成模块。

1
2
cd out/artifaces/module_A_jar/
jmod create --class-path module-A.jar module-A.jmod

最终在jar包同级目录下生成一个module-A.jmod文件。这便是最终的模块。

3.运行模块

要运行一个jar,要使用java -jar module-A.jar,那么我们该如何运行一个模块呢,我们先看一下java命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#: java
用法:java [options] <主类> [args...]
(执行类)
或 java [options] -jar <jar 文件> [args...]
(执行 jar 文件)
或 java [options] -m <模块>[/<主类>] [args...]
java [options] --module <模块>[/<主类>] [args...]
(执行模块中的主类)
或 java [options] <源文件> [args]
(执行单个源文件程序)

将主类、源文件、-jar <jar 文件>、-m 或
--module <模块>/<主类> 后的参数作为参数
传递到主类。

...

可以看到,要执行模块中的主类,我们需要使用-m或者--module来指定模块和主类。

1
java -m module.A/top.Main

运行命令,发现报错:

错误信息是找不到module.A模块,加上--module-path参数,来指定我们的模块。

1
java --module-path module-A.jmod -m module.A/top.Main

运行,还是报错
img.png

原因是.jmod不能被放入--module-path中,换成.jar就没问题了。

1
2
3
java --module-path module-A.jar -m module.A/top.Main
5月 21, 2024 10:50:20 上午 top.Main main
信息: Hello World!

也可以这样写:

1
2
3
java --module-path ./ -m module.A/top.Main
5月 21, 2024 10:50:20 上午 top.Main main
信息: Hello World!

但我们要明白,具体起作用的是.jar文件,而不是.jmod文件。

那么我们打包出来的.jmod文件有什么用呢?答案是用来打包jre

4.打包jre

jlink是java9之后提供的一个工具。官方说明:

jlink

The jlink tool links a set of modules, along with their transitive dependences, to create a custom runtime image.

我们可以使用jlink打包jre来作为运行时环境。因为java9开始,java已经都实现了模块化,所以打包中可以只使用必须要的模块。

1
jlink --module-path module-A.jmod --add-modules java.base,java.logging,module.A --output jre/

--module-path参数指定模块的路径,--add-modules参数指定要打包的模块,多个模块使用,分隔。--output参数指定输出路径。

现在我们的目录下可以看到jre目录了。
img.png

我们可以使用jre目录下的bin/java来直接运行我们的模块。而不需要再使用--module-path指定模块路径。

1
2
3
jre/bin/java -m module.A/top.Main
May 21, 2024 11:12:14 AM top.Main main
信息: Hello World!

如果我们要分发这个程序,只需要把jre目录打个压缩包即可。对方直接运行上边命令即可,不再需要安装java环境,也不需要知道如何配置模块,极大的方便了分发和部署。

5.module-info.java

回过头来,我们再看一下module_info.java文件。

module_info.java
1
2
3
4
module module.A {
// 依赖java.logging模块
requires java.logging;
}

module_info.java文件用来声明模块的依赖关系。module关键字声明模块。requires关键字用来声明依赖的模块。没有声明依赖的模块,是无法使用的。所有的模块默认都依赖java.base, 可省略不写。
那么如果我们要把创建的模块给其它模块使用,该如何写呢?我们可以看下java.logging模块的module_info.java文件。

module_info.java
1
2
3
4
5
6
module java.logging {
exports java.util.logging;

provides jdk.internal.logger.DefaultLoggerFinder with
sun.util.logging.internal.LoggingProviderImpl;
}

可以看到,使用exports关键字来声明模块的导出包。provides关键字用来声明模块的提供者。
这里只声明导出了java.util.logging包,所以我们只能访问java.util.logging包,而不能访问其它包中的类。
简单说下module-info.java中的关键字。

module

模块声明。

module-info.java
1
2
module moduleName{
}

模块声明的正文可以留空,也可以包含各种指令,比如requiresexportsprovides等。

requires

指定此模块依赖于另一个模块,每个模块必须明确指出其依赖项。

module-info.java
1
requires moduelName;

requires static

用于指示在编译时必须要有模块,但运行时并不需要。

requires transitive

requires transitive关键字用来声明模块的依赖关系,并且会传递给依赖它的模块。例如A模块依赖B模块,B模块依赖C模块,那么使用requires transitive,A模块就可以直接使用C模块中的类。

exports

exports关键字用来声明模块的导出包。

exports…to

exports...to关键字用来声明模块的导出包,并且指定导出给哪些模块。

uses

uses关键字用来声明模块的接口。

provides…with

provides关键字用来声明模块的提供者。指令的 with 部分指定 implements 接口或 extends abstract类的服务提供类的名称。

open

模块本身可以被open修饰。表示该模块是开放的。

opens

opens关键字用来声明模块的开放包。

opens…to

opens关键字用来声明模块的开放包,并且指定开放给哪些模块。

不具名模块和自动模块

java9之后,jdk实现了模块化,那么我们是否也都需要模块化呢?答案是否定的。

我们依然可以延续以往的开发模式,而不使用模块化。

使用idea重新创建一个java项目module-B

module-B结构
module-B结构

修改Main.java:

Main.java
1
2
3
4
5
6
7
8
import java.util.logging.Logger;

public class Main {
public static void main(String[] args) {
Logger logger = Logger.getLogger("Main");
logger.info("Hello World!");
}
}

运行main方法

运行结果
运行结果

可以看到,没有module-info.java文件,依然可以使用java.util.logging包。
那么这个普通工程可以打包成模块吗?
我们来尝试一下。

首先,构建一个可执行的jar文件,然后借助jmod打包:

1
2
3
# 测试jar包可以正常执行
java -jar module-B.jar
jmod create --class-path module-B.jar module-B.jmod

运行报错,找不到module-info.class文件

jmod create --class-path module-B.jar module-B.jmod
jmod create --class-path module-B.jar module-B.jmod

可以看到,这样是不行的。但我们依然就可以使用java命令以模块的方式来运行module-B.jar;

1
java --module-path module-B.jar -m module.B/top.Main
模块方式运行
模块方式运行

实际上,没有module-info.java的jar包可以称之为 不具名模块,不具名模块放在module-path中,则会变成自动模块
允许 Java 9 模块能引用到这些模块中的类。自动模块对外暴露所有的包并且能引用到其他所有模块的类,其他模块也能引用到自动模块的类。由于自动模块并不能声明模块名,那么 JDK 会根据 jar 包名来自动生成一个模块名以允许其他模块来引用。生成的模块名按以下规则生成:首先会移除文件扩展名以及版本号,然后使用”.“替换所有非字母字符。
例如module-B.jar会生成名为module.B的模块名。

访问权限

模块化之后,访问权限发生了改变。对于具名模块而言,所有的类都是私有的,只有模块声明的包或类才能被其他模块访问。而对于自动模块而言,所有的包都对外暴露,和java9之前并没有什么不同。

参考