模块化
好处
- 强封装
- 显示依赖
- 得益于模块化,衍生了jlink等技术
缺点
- 不向后兼容,非模块的jar,上手难度较大
- 生态是把双刃剑
如何使用
在项目src目录下创建module-info.java文件,有没有发现 名称居然带-,-在java中不能在-命名,但是为了防止非模块感知工具盲目地将module-info.java或module-info.class作为普通的Java类加以处理。 默认情况下,所有的包为强封装,别人不能访问到该模块的任何东西,隐式依赖java.base模块
module 模块名 {
}
模块命名
模块名称与java的命名空间是分开的,模块名称必须唯一
参数说明
open 修饰符
open修饰符是可选的,它声明一个开放的模块,一个开放的模块导出所有的包,以便其他模块使用反射访问。是要定义的模块的名称,是一个模块语句。模块声明中可以包含零个或多个模块语句:
exports
导出语句,给其他模块访问。
exports 包名
限制导出
exports 包名 to 模块名
opens
开放语句(opens),开放当前模块,如果模块没有exports导出项目,可以反射调用等。
requires
声明模块对另一个模块的依赖关系。
requires 模块
依赖传递 A —>B---->C A是不能访问到C的需要添加transitive 添加 transitive 表示为具有传递性(隐式可读性),如果是项目 ,记得在idea 项目中的modules 设置Dependencies
requires transitive 模块
巧用 transitive 实现聚合器模式
module library {
requires transitive library.A;
requires transitive library.B;
requires transitive library.C;
requires transitive library.D;
}
使用static修饰,表示这个依赖的编译时需要
requires static 模块
uses
使用什么 ,表达服务消费。一般为接口 搭配ServiceLoader来加载
举个例子 模块C
open module C {
exports packC;
}
public interface InterF {
public void hello();
}
模块D
module D {
requires C;
provides packC.InterF with Implement;
}
核心代码
public class Implement implements InterF {
@Override
public void hello() {
System.out.println("hhh");
}
}
模块A
module A {
exports A;
exports pack;
requires C;
uses packC.InterF;
requires D;
}
主要代码
public static void main(String[] args) {
ServiceLoader<InterF> interFS =
ServiceLoader.load(InterF.class);
interFS.stream().forEach((interFProvider) -> {
System.out.println(interFProvider.type().getFields().length);
interFProvider.get().hello();
});
}
}
输出: hhh
注意:如果模块A中没有使用 uses packC.InterF; 还使用了ServiceLoader来找InterF 则会失败
provide
用于提供服务 provides 接口 with 具体实现 刚刚的D模块
module D {
requires C;
provides packC.InterF with Implement;
}
为什么要搞个消费和提供? 用于将接口和实现分离,当一个公共的接口有多个实现时,将api和实现分开,调用层只需user 需要的api 而无需关心具体实现 模块中的包名不能相同
用原生命令对模块进行编译打包和运行
编译 javac文档路径
javac -d 输出文件 需要编译的文件路径 module-info路径
打包
jar -cfe 路径/名称.jar 主程序路径 -C 编译文件存放的路径
运行
java -p 模块路径 -m 模块名称
-p == --module-path
-m == --module
如果模块路径 为 class路径 则 -m后为模块名/主程序类路径 多模块路径用分隔符隔开,linux、macOs用:,windows用;
例子
Microsoft Windows [版本 10.0.19043.1586]
(c) Microsoft Corporation。保留所有权利。
D:\study\JDKStudyN\C>javac -d out src/packC/C.java src/module-info.java
D:\study\JDKStudyN\C>jar -cfe mods/c.jar packC.C -C out
解析文件参数时出错
尝试使用 `jar --help' 获取详细信息。
# 注意后面 有个空格 和点
D:\study\JDKStudyN\C>jar -cfe mods/c.jar packC.C -C out .
D:\study\JDKStudyN\C>java -p mods -m C
hhh
D:\study\JDKStudyN\C>
自定义运行时镜像
使用jlink 可以创建应用程序所需要的运行时镜像
常用参数
- –add-modules mod[, mod…] 将命名模块 , 添加mod到默认的根模块集中。默认的根模块集是空的。
- –bind-services 链接服务提供者模块及其依赖项。
- -c={0|1|2}要么–compress={0|1|2} 0:无压缩,1:常量字符串共享,2:压缩
- –no-header-files 不包括头文件。
- -p要么–module-path modulepath 指定模块路径。
如果未指定此选项,则默认模块路径为
J
A
V
A
H
O
M
E
/
j
m
o
d
s
.
该
目
录
包
含
j
a
v
a
.
b
a
s
e
模
块
以
及
其
他
标
准
和
J
D
K
模
块
。
如
果
指
定
了
此
选
项
,
但
j
a
v
a
.
b
a
s
e
无
法
从
中
解
析
模
块
,
然
后
j
l
i
n
k
命
令
附
加
JAVA_HOME/jmods. 该目录包含java.base模块以及其他标准和 JDK 模块。如果指定了此选项,但java.base无法从中解析模块,然后jlink命令附加
JAVAH?OME/jmods.该目录包含java.base模块以及其他标准和JDK模块。如果指定了此选项,但java.base无法从中解析模块,然后jlink命令附加JAVA_HOME/jmods到模块路径。 - –no-man-pages 不包括手册页。
- –output path 指定生成的运行时映像的位置。
- –save-opts filename 将选项保存jlink在指定文件中。
- –launcher 一个启动的脚本程序 --launcher name=module/main-class
- name是为可执行文件的名称;
- main-class可以省略
例子: 还是我们刚刚的项目
jlink -p mods/;$JAVA_HOME/jmods --add-modules C --launcher hello=C --output C-image
加载模块资源
因为模块化的存在,通过class.getModule获取所在模块
Class clazz = ResourcesInModule.class;
InputStream cz_pkg = clazz.getResourceAsStream("/resource_in_package.txt"); //<1>
if (cz_pkg!=null)
System.out.println(new String(cz_pkg.readAllBytes()));
URL cz_tl = clazz.getResource("/top.txt"); //<2>
System.out.println(cz_tl);
Module m = clazz.getModule(); //<3>
InputStream m_pkg = m.getResourceAsStream(
"resource_in_package.txt"); //<4>
InputStream m_tl = m.getResourceAsStream("top.txt"); //<5>
System.out.println(new String(m_tl.readAllBytes()));
System.out.println(new String(m_pkg.readAllBytes()));
加载其它模块的资源呢?
Optional<Module> otherModule = ModuleLayer.boot().findModule("C"); //<1>
otherModule.ifPresent(other -> {
try {
InputStream resourceAsStream = other.getResourceAsStream("packC/C.class"); //<1> 可以获取到
System.out.println(resourceAsStream.available());
InputStream pkg = other.getResourceAsStream(
"packC/in_package.txt"); //<2> 包中资源封闭的为null
InputStream m_pkg = other.getResourceAsStream(
"top_in_package.txt"); //<3>
System.out.println(new String(m_pkg.readAllBytes()));
System.out.println(new String(pkg.readAllBytes()));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
2步骤是获取不到的,包中的资源都是封闭的,但是.class例外,不过要注意不应该依赖其它模块,这样会关系变乱 在 module 上增加open 可开放包
实战
目标创建一个最小的跑Vert.x的JVM runtime image
创建 Maven项目
maven 中使用 模块化和以前的maven项目没什么区别,添加module-info.java 如果jar为自动化路径则他的名称一般为包名去掉版本号然后横杠转化为. 还可以借助强大的Idea,它会提示你
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jmodsTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<vertx.version>4.0.0</vertx.version>
<main.class>io.vertx.core.Launcher</main.class>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>${vertx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<includeScope>runtime</includeScope>
<excludeArtifactIds>
</excludeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<!--打包后的启动入口 -->
<Main-Class>${main.class}</Main-Class>
<Main-Verticle>com.mods.app.App</Main-Verticle>
</manifestEntries>
</transformer>
</transformers>
<artifactSet />
<outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar</outputFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
module-info
module jmodsTest {
requires io.vertx.web;
requires io.vertx.core;
exports com.mods.app;
}
Main-Class
public class App extends AbstractVerticle {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new App())
.onFailure(Throwable::printStackTrace);
}
@Override
public void start(Promise<Void> startPromise) throws Exception {
vertx.createHttpServer().requestHandler(req -> {
req.response()
.putHeader("content-type", "text/plain")
.end("Hello World!");
}).listen(8080);
}
}
自定义运行时镜像
Maven 中使用 Jlink插件
Jlink插件 使用Jlink 项目中的包不能是非模块的jar,Jlink不支持
非模块Jar转化为模块化
1.导出需要转化的jar
基于dependency插件,使用如下配置,在打包时会将jar导入项目build/modules目录中
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<includeScope>runtime</includeScope>
<excludeArtifactIds>
</excludeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>
2.转化步骤代码
public static void main(String[] args) throws Exception {
//jdeps --module-path D:\study\JDKStudyN\jmodsTest\target\modules --multi-release 11 --ignore-missing-deps --generate-module-info . D:\study\JDKStudyN\jmodsTest\target\modules\vertx-web-4.0.0.jar
String modulePath = "D:\\study\\JDKStudyN\\jmodsTest\\target\\modules";
String javaHome="C:\\Users\\NINGMEI\\.jdks\\azul-11.0.14";
String moduleInfoSavePath = modulePath + "\\mod";
String projectJar="jmodsTest-1.0-SNAPSHOT.jar";
Files.list(Path.of(modulePath)).forEach((path) -> {
String jarPath = path.toString();
String jarName = path.getFileName().toString();
if (jarName.equals(projectJar)||path.toFile().isDirectory()) {
return;
}
generateModuleInfo( javaHome, jarPath, modulePath, moduleInfoSavePath);
});
}
private static void generateModuleInfo(String javaHome, String jarPath, String modulePath, String moduleInfoSavePath) {
System.out.println(String.format("开始创建ModuleInfo{%s}",jarPath));
Path bin = Path.of(javaHome, "bin");
List<String> cmds = List.of("jdeps",
"--module-path",
modulePath,
"--multi-release",
"11",
"--ignore-missing-deps",
"--generate-module-info",
moduleInfoSavePath,
jarPath
);
executeCmd(bin.toString(), cmds, (msg) -> {
System.out.println(msg);
msg.lines().reduce((first, second) -> second).ifPresent((m) -> {
if (m.contains("writing")) {
String moduleInfoPath = m.replace("writing to ", "");
List<String> mInfo = FileUtil.readLines(moduleInfoPath, StandardCharsets.UTF_8);
String moduleName = mInfo.get(0).replace("module", "").replace("{", "").trim();
compileModuleInfo( javaHome, modulePath, jarPath, moduleName, moduleInfoPath);
}
});
});
}
private static void compileModuleInfo(String javaHome, String modulePath, String jarPath, String moduleName, String moduleInfoPath) {
System.out.println(String.format("开始编译{%s}",jarPath));
Path jmods = Path.of(javaHome, "jmods");
Path bin = Path.of(javaHome, "bin");
List<String> cmds = List.of("javac",
"--module-path",
String.format("%s;%s", modulePath, jmods),
"--patch-module",
String.format("%s=%s", moduleName, jarPath),
moduleInfoPath
);
executeCmd(bin.toString(), cmds, (msg) -> {
System.out.println(msg);
updJar( javaHome, jarPath, moduleInfoPath);
});
}
private static void updJar(String javaHome,String jarPath,String moduleInfoPath) {
System.out.println(String.format("开始更新jar{%s}",jarPath));
Path bin = Path.of(javaHome, "bin");
List<String> cmds3 = List.of("jar",
"uf",
jarPath,
"-C",
StrUtil.subBefore(moduleInfoPath, "\\module-info.java", false),
"module-info.class");
executeCmd(bin.toString(), cmds3, (msg2) -> {
});
}
private static void executeCmd(String directory, List<String> cmd, Consumer<String> msgHandle) {
ProcessBuilder processBuilder = new ProcessBuilder();
// setting up a working directory
processBuilder.directory(new File(directory));
processBuilder.command(cmd);
Process process = null;
try {
processBuilder.redirectErrorStream(true);
process = processBuilder.start();
process.waitFor(3, TimeUnit.SECONDS);
msgHandle.accept(getUtf8String(IoUtil.readBytes(process.getInputStream())));
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public static String getUtf8String(byte[] bytes) throws UnsupportedEncodingException {
String encode = "GBK";
boolean pdUtf = false;
pdUtf = validUtf8(bytes);
if (pdUtf) {
encode = String.valueOf(StandardCharsets.UTF_8);
}
return new String(bytes, encode);
}
public static boolean validUtf8(byte[] data) {
int i = 0;
int count = 0;
while (i < data.length) {
int v = data[i];
if (count == 0) {
if ((v & 240) == 240 && (v & 248) == 240) {
count = 3;
} else if (((v & 224) == 224) && (v & 240) == 224) {
count = 2;
} else if ((v & 192) == 192 && (v & 224) == 192) {
count = 1;
} else if ((v | 127) == 127) {
count = 0;
} else {
return false;
}
} else {
if ((v & 128) == 128 && (v & 192) == 128) {
count--;
} else {
return false;
}
}
i++;
}
return count == 0;
}
一些警告的可以忽略 当出现错误:程序包XX不存在
D:\study\JDKStudyN\jmodsTest\target\modules\mod\io.netty.common\versions\11\module-info.java:16: 错误: 程序包reactor.blockhound.integration不存在
provides reactor.blockhound.integration.BlockHoundIntegration with
^
1 个错误
手动将这个jar放到modules目录下,然后执行一下脚本就ok了
3.打包运行时镜像
jlink --module-path $JAVA_HOME/jmods;modules --add-modules jmodsTest --launcher run=jmodsTest/com.mods.app.App --output image
4.启动
进入image文件和本地的结构相似,直接进入bin然后cmd启动run.bat
启动成功!!!
使用 list-modules 可以看jvm中包含的modules
java --list-modules
输出:
com.fasterxml.jackson.core@2.11.3
io.netty.buffer
io.netty.codec
io.netty.codec.dns
io.netty.codec.http
io.netty.codec.http2
io.netty.codec.socks
io.netty.common
io.netty.handler
io.netty.handler.proxy
io.netty.resolver
io.netty.resolver.dns
io.netty.transport
io.vertx.auth.common
io.vertx.core
io.vertx.eventbusbridge.common
io.vertx.web
io.vertx.web.common
java.base@11.0.14
java.compiler@11.0.14
java.instrument@11.0.14
java.logging@11.0.14
java.management@11.0.14
java.naming@11.0.14
java.security.sasl@11.0.14
jdk.unsupported@11.0.14
jmodsTest@1.0-SNAPSHOT
reactor.blockhound
只有65M,jdk8 的jre 108M不好含运行jar
5.启动时或者运行时出现 XX: module XX does not declare uses
这是因为项目中使用了ServiceLoader 如果忘了uses 可以跳到前面看一下。
警告: Default DNS servers: [/8.8.8.8:53, /8.8.4.4:53] (Google Public DNS as a fallback)
Exception in thread "main" java.util.ServiceConfigurationError: io.vertx.core.spi.VerticleFactory: module io.vertx.core does not declare `uses`
说明jdeps在生成的时候没有分析到,这时候需要我们手动修改模块 io.vertx.core的module-info
.....省略
uses io.vertx.core.spi.VerticleFactory;
然后使用刚刚转化步骤中的compileModuleInfo 重新编译更新jar或者手动输入命令
javac --module-path $JAVA_HOME/jmods;modules --patch-module io.vertx.core=D:\study\JDKStudyN\jmodsTest\target\modules\vertx-core-4.0.0.jar D:\study\JDKStudyN\jmodsTest\target\modules\mod\io.vertx.core\versions\11\module-info.java
jar uf modules\vertx-core-4.0.0.jar -C modules\mod\io.vertx.core\versions\11 module-info.class
然后重新jlink一下就ok
极致压缩
有人说60M和100M差不多啊没什么区别啊
jlink --module-path $JAVA_HOME/jmods;modules --no-header-files --no-man-pages --compress=2 --add-modules jmodsTest --launcher run=jmodsTest/com.mods.app.App --output miniImage
有人说:磁盘现在不怎么要钱啊! 其实这意味着jvm可以和项目捆绑发布,做到用户无感知。适应云原生
创建不同平台的运行时镜像
下载你需要的jdk 并解压,然后将module-path 指向下载的jdk中的jmods即可
参考:
深入理解java模块化系统 Java9模块化开发:核心原则和实践 jdk11 官方文档
|