CONCEPT

在java9,java引入了一种不同于,或者可以说是在 package 之上的一个新的抽象层–> JPMS。

什么是module。 module是一系列相关的package与resource的集合

module的两种表现形式: jar已编译项目

Module-Properties

  • name : Module的name命名方式与package相同,goupId反写。
  • requires | uses Module所依赖的其他模块 | 服务。所有模块都直接或者间接依赖java.base
  • exportsModule公开的可以被其他模块依赖的 package
  • provides Module 提供的服务
  • open<module level> | opens<package level> 其他module对私有包的反射权限

Module-Type

  • 系统Module: 包括根据javaSE标准实现的java开头的module和 jdk内部使用的module
  • 应用级别Module: 具有module-info.java描述文件的我们自定义与第三方jar或者编译项目
  • 自动Module: 被添加在--module-path模块路径上的没有module-info.javajar,他们通常取 example-1.0-GA.jar中的example作为module名,可以用jdeps查看到
  • 未命名Module: 被添加在 --class-path 类路径上的 class或者 jar都被加到 Unnamed Module

module-info.java

参考 baeldung-java-modularity-module-declarations

Examples

Simple-Multiple-Modules

参考 baeldung

Maven-Multiple-Modules

Cteate-Maven-Project

jdk为openjdk17。尝试构建带有其他依赖的maven模块化项目,包括未模块化的依赖与已经模块化的依赖。

Project-Struct

screen-capture

与之前不同的是,在java源码目录下,多了module-info.java文件。

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.10</version>
</dependency>
</dependencies>

由于是模块化的project,所以如果依赖不仅要在pom.xml中声明引入,并且只要创建了 module-info.java文件,就要在其中写入相关配置,否则会编译失败。

module-info.java
1
2
3
4
5
module moduleA {
exports world.langyu.provider;
requires org.mybatis;
requires com.alibaba.fastjson2;
}
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package world.langyu.provider;

import com.alibaba.fastjson2.JSON;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

public class Main {

public static void main(String[] args) throws IOException {
System.out.println("work dir: " + System.getProperty("user.dir"));
run();
}

public static void run() throws IOException {
Configuration configuration = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"))
.getConfiguration();
System.out.println("moduleA");
System.out.println("none modularity dep: configuration of mybatis: "+ configuration.getVariables());
System.out.println("modularity dep: fastjson: "+ JSON.class.getName());
}

}

目前看起来没有任何问题,然后编译

Compile

在模块根部录下:

javac -d .\target\classes -p "D:\maven_local_repository\com\alibaba\fastjson2\fastjson2\2.0.10\fastjson2-2.0.10.jar;D:\maven_local_repository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar" .\src\main\java\module-info.java .\src\main\java\world\langyu\provider\*.java

其中

  • -d选项指定编译的输出目录。
  • -p/--module-path选项 指定应用代码依赖的外部模块的路径,通常是jar包路径。注意在windows下,参数要用""包起来,并且多个路径之间用;隔开。
  • 由于是单模块,后跟 module-info.java文件的路径。
  • 最后列出要编译的.java文件的路径

emmmm,不出意外的话就要出意外了=_=

1
2
3
4
.\src\main\java\module-info.java:4: 错误: 找不到模块: com.alibaba.fastjson2
requires com.alibaba.fastjson2;
^
1 个错误

顺着这个报错就找到了fastjson2的github的issue,然后是他们的一个bug,本来生个版本就可以解决了,但我想看看到底是个什么问题->fastjson2-issue

维护的人修改这个bug是去修改了core/pom.xml

screen-capture

原来 fastjson2使用的一个第三方插件来做的模块化,它的编译产物还是java8的。

换完版本之后顺利编译

screen-capture

因为我们要依赖一些静态配置资源,copy过去。

运行

Non-modular-Mode

执行: java -cp ".\target\classes;D:\maven_local_repository\com\alibaba\fastjson2\fastjson2\2.0.15\fastjson2-2.0.15.jar;D:\maven_local_repository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar" world.langyu.provider.Main

  • cp指定所有涉及到的类路径。
  • 最后带上主类的全限定名。

screen-capture

run-as-simple-jar

pom.xml中配置mainclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>world.langyu.provider.Main</mainC
lass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>

首先要打成jar包->mvn clean package

执行:java -p "D:\maven_local_repository\com\alibaba\fastjson2\fastjson2\2.0.15\fastjson2-2.0.15.jar;D:\maven_local_repository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar" --add-modules com.alibaba.fastjson2,org.mybatis -jar .\target\moduleA-1.0-SNAPSHOT.jar

注意:

  • -p指定所依赖的jar的路径,-p的全称是--module-path,虽说是module,但是 JPMS中,module的分发产物依然是jar形式。
  • fastjson2是个模块化的jar,可以在 fastjson2-2.0.15.jar!\META-INF\versions\9\module-info.class找到模块描述文件,里面自然有module-name,所以在后面--add-modules指定的参数中就可以指定其定义的com.alibaba.fastjson2。但奇怪的是,mybatis分发的jar中我并没有找到module-info.java,这个名称我是从在编写moduleAmodule-info.java文件时,idea提示我的所要requires的module-name,但按理来说,如果你没有模块化,那就会转为Automatic-Module,模块名默认是xxxlib-1.0-xx.jar中的xxxlib,也就是版本号前面的内容,但这时的mybatis并符合预期。

于是我顺藤摸瓜,去翻了mybatis的jar,然后在其中的META_INF/MANIFEST.MF中找到了

86a11f912c46fe4bc8d51af9602e27d8.png

有种解密的味道哈哈哈—~~~

原来mybatis这个编译的版本是jdk17,看来mybatis模块化的做法很简单:

利用了 Automatic-Module机制:

  1. 它没有module-info.java文件,当被其他开发者作为依赖添加到模块路径上(-p)时,他会成为一个自动模块:1. export and open all packages 2. have full read access to every other module loaded by the module path
  2. 通过在 MANIFEST.MF文件中指定Automatic-Module-Name来覆盖默认的module-name。
  3. 要做到 2. 可以在pom.xml中配置maven-jar-plugin
1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>${module.name}</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>

然后其他模块就可以用${module.name}来生命对这个模块的依赖。

顺带提一嘴,我在测试的时候发现,idea可以在你指定了Automatic-Module-Name或者这个模块的 artifactId(这个是因为如果不指定jar的名称,默认是${artifactId}-{version}.jar)然后reload maven projects后 提示或识别出来的module-name。

一直很想找到能这样做的出处(在MANIFEST.MF中指定 Automatic-Module-Name),推测是JEP讨论出来的。

编译与运行中有关Unnamed-Module的问题

什么是Unnamed-Module

被添加到–class-path中的classes与jar都会被加到Unnamed-Module中。

Unnamed-Module具有如下特性:

  1. Export and open all its packages to every other module, like the Automatic Module.
  2. Other module can only access classes in Unname-Module by reflection
  3. The declaration of dependencies by the unnamed module on other modules is futile at runtime.

未命名模块就是所有放在-classpath上的类、模块的一个大杂烩模块。因为是未命名,所以不能被其他模块直接依赖,其他模块只能通过反射的方式访问其中的类,对应1、2两点。

关于第三点:未命名模块中对外部声明的依赖(即使是在module-info.java中生命的 require)不会被模块系统自动解析和加载,除非此时未命名模块是root module其中之一。

问题复现

比如:现在有A,B两个jar,jarA,jarB都具有module-info.java,也就是都是模块化后的jar,jarB代码层面只依赖于jarA()。

jarB:

1
2
3
module moduleB {
}

jarA:

1
2
3
4
module moduleA {
requires org.mybatis;
}

执行: java -classpath D:\source-read\java-module\moduleA\target\moduleA-1.0-SNAPSHOT.jar -p "D:\source-read\java-module\moduleB\target\moduleB-1.0-SNAPSHOT.jar;D:\maven_local_repository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar" -m moduleB/world.langyu.consumer.Main

注意此时我们将 jarA放在了 类路径上,所以jarA是Unnamed-Module的一部分。所以即使moduleA声明了对mybatis的依赖,但还是会得到如下报错:

1
2
3
4
5
6
Caused by: java.lang.ClassNotFoundException: org.apache.ibatis.session.SqlSessionFactoryBuilder
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
... 6 more

问题分析

在启动参数上加上--show-module-resolution来查看 modules resolved during app startup

1
2
3
4
5
6
7
root moduleB file:///D:/source-read/java-module/moduleB/target/moduleB-1.0-SNAPSHOT.jar
java.base binds java.desktop jrt:/java.desktop
java.base binds jdk.security.auth jrt:/jdk.security.auth
java.base binds java.management jrt:/java.management
java.base binds jdk.localedata jrt:/jdk.localedata
java.base binds java.logging jrt:/java.logging
...

看来mybatis的模块并没有被解析加载到运行时环境中。

这就要提到java模块化系统中的模块发现机制(module resolution

  • java.base是所有模块的根模块,不需要显式声明。
  • 编译与运行中,模块系统需要先确定一个或者多个root-module,然后根据这些root-module的依赖关系去-p指定的模块路径下寻找所有可以观察到的模块(observable module)然后解析加载以满足依赖,再加上-cp指定的类路径,就构成了编译与运行时环境。
  • 通过-m方式启动,-m指定的模块就是root-module之一。
  • 如果是传统方式java [options] <主类> [args...],则root-module 就是 java.*java.basejava.sql…)模块,也就是JRE

在这个例子中(通过-m),moduleB是root-module,moduleB、java.base、jarA构成了运行时环境(注意即使mybatis模块也在模块路径上,但运行时环境不包含他。),因为jarA被添加到了类路径上,是个Unnamed-Module,不会有其它模块对他有显式的依赖,并且他也不可能是root-module,所以是不会去解析Unnamed-Module的依赖关系的,自然mybatis模块没有被加入到运行时环境中。

解决方案

这时就需要手动把org.mybatis从模块路径上添加到被解析模块中:-add-modules org.mybatis, java.sql。(mybatis模块还依赖于java.sql)

或者

直接采用传统方式启动,把类和jar都放在类路径上:java -classpath "D:\source-read\java-module\moduleA\target\moduleA-1.0-SNAPSHOT.jar;D:\source-read\java-module\moduleB\target\moduleB-1.0-SNAPSHOT.jar;D:\maven_local_repository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar" world.langyu.consumer.Main


结束哩 😹

其他相关