包阅导读总结
1.
关键词:Java、模块化、模块系统、依赖管理、性能提升
2.
总结:本文介绍了 Java 从 Java 8 到 Java 9 及更高版本引入的模块系统这一新特性,包括其概念、优势、关键指令、参数等,还阐述了迁移应用的策略和过程,以及处理老版本应用的方法。
3.
主要内容:
– 模块系统的引入
– 目的:解决依赖管理等问题,提升性能等
– 历史:最早于 2008 年提出,2014 年随 Java 9 开发,2017 年随 Java 9 发布
– 模块系统的概念
– 模块载体:jar 文件,多了 module-info.class
– 带来好处:计算依赖关系、精准定位包、缩小 JRE 大小、细化类可见性
– 模块的核心
– module descriptor:包含模块相关信息
– 专用语法:如 module、requires 等关键词
– 模块示例
– 多个模块的代码及编译运行命令
– 新参数
– -p:指定模块路径
– -m:指定待运行的模块主函数
– 迁移应用
– 自底向上策略及问题
– 自上而下分类改造
思维导图:
文章地址:https://mp.weixin.qq.com/s/n1ZxaBSlDP-_VfvWwsFB5A
文章来源:mp.weixin.qq.com
作者:阶行
发布时间:2024/7/11 9:31
语言:中文
总字数:6989字
预计阅读时间:28分钟
评分:91分
标签:Java模块化,微服务架构,Java 9新特性,依赖管理,JRE优化
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
这是2024年的第49篇文章
( 本文阅读时间:15分钟 )
Java平台从Java 8向Java 9及更高版本的进化,其中引入了一个重要的新特性——模块系统(Project Jigsaw)。模块系统的目的是解决大型应用的依赖管理问题,提升性能,简化JRE,增强兼容性和安全性,并提高开发效率。通过模块化,Java能够更好地支持微服务架构,提供更细粒度的封装和控制,以及更清晰的依赖关系。
文章详细介绍了模块系统的概念,如MODULE DESCRIPTOR、主要参数、关键指令,以及模块化策略。此外,本文还提供了最佳实践建议,帮助开发者更好地理解和应用Java模块系统。
Auniquelynamed,reusablegroupofrelatedpackages,aswellasresources(suchasimagesandXMLfiles)andamoduledescriptor.
-
Java 可以根据 module descriptor 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。
-
由于模块系统不允许不同模块导出相同的包(即split package,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。
-
mod1 模块: 主模块,展示了使用服务实现类的两种方式。
importmod3.exports.IEventListener;
module mod1 {
requires mod2a;
requires mod4;
uses IEventListener;
}
packagemod1;
import mod2a.exports.EchoListener;
import mod3.exports.IEventListener;
import mod4.Events;
import java.util.ArrayList;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
public class EventCenter {
System.out.println("Demo: Direct Mode");
var listeners = new ArrayList<IEventListener>();
listeners.add(new EchoListener());
listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance());
var event = Events.newEvent();
listeners.forEach(l -> l.onEvent(event));
System.out.println();
System.out.println("Demo: SPI Mode");
var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());
var event2 = Events.newEvent();
listeners2.forEach(l -> l.onEvent(event2));
}
#!/bin/zsh
# mod4
javac -d out/mods/mod4 mod4/src*.java
jar -cvf out/mods/mod4.jar -C out/mods/mod4 .
# mod3
javac -d out/mods/mod3 mod3/src*.java
jar -cvf out/mods/mod3.jar -C out/mods/mod3 .
# mod2b
javac -p out/mods/mod3.jar -d out/mods/mod2b mod2b/src*.java
jar -cvf out/mods/mod2b.jar -C out/mods/mod2b .
# mod2a
javac -p out/mods/mod3.jar -d out/mods/mod2a mod2a/src*.java
jar -cvf out/mods/mod2a.jar -C out/mods/mod2a .
# mod1
javac -p out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -d out/mods/mod1 mod1/src*.java
jar -cvf out/mods/mod1.jar -C out/mods/mod1 .
# run
java -p out/mods/mod1.jar:out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -m mod1/mod1.EventCenter
-
mod2a 模块: 分别导出和开放了一个包,并声明了两个服务实现类。
importmod3.exports.IEventListener;
module mod2a {
requires transitive mod3;
exports mod2a.exports;
opens mod2a.opens;
provides IEventListener
with mod2a.exports.EchoListener, mod2a.opens.ReflectEchoListener;
}
packagemod2a.exports;
import mod3.exports.IEventListener;
public class EchoListener implements IEventListener<String> {
@Override
public void onEvent(String event) {
System.out.println("[echo] Event received: " + event);
}
}
package mod2a.opens;
import mod3.exports.IEventListener;
public class ReflectEchoListener implements IEventListener<String> {
@Override
public void onEvent(String event) {
System.out.println("[reflect echo] Event received: " + event);
}
}
-
mod2b 模块: 声明了一个未公开的服务实现类。
importmod3.exports.IEventListener;
module mod2b {
requires transitive mod3;
provides IEventListener
with mod2b.SpiEchoListener;
}
packagemod2b;
import mod3.exports.IEventListener;
public class SpiEchoListener implements IEventListener<String> {
@Override
public void onEvent(String event) {
System.out.println("[spi echo] Event received: " + event);
}
}
-
mod3 模块: 定义 SPI 服务(IEventListener),并声明了一个未公开的服务实现类。
importmod3.exports.IEventListener;
module mod1 {
requires mod2a;
requires mod4;
uses IEventListener;
}
packagemod3.exports;
public interface IEventListener<T> {
void onEvent(T event);
}
package mod3.internal;
import mod3.exports.IEventListener;
public class InternalEchoListener implements IEventListener<String> {
@Override
public void onEvent(String event) {
System.out.println("[internal echo] Event received: " + event);
}
}
modulemod4{
exports mod4;
}
packagemod4;
import java.util.UUID;
public class Events {
public static String newEvent() {
return UUID.randomUUID().toString();
}
}
-
Java 9 引入了一系列新的参数用于编译和运行模块,其中最重要的两个参数是 -p 和 -m。-p 参数指定模块路径,多个模块之间用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同时适用于 javac 命令和 java 命令,用法和Java 8 中的 -cp 非常类似。-m 参数指定待运行的模块主函数,输入格式为模块名/主函数所在的类名,仅适用于 java 命令。两个参数的基本用法如下:
-
javac -p <module_path> <source> -
java -p <module_path> -m <module>/<main_class>
-
最快的速度判别它是不是一个模块:jar -d -f <jar_file>
-
-
如项目依赖Java EE的相关xml代码, 编译和运行时就需要添加对应的模块javac –add-moudles java.xml.bind
-
-
-
当迁移过程中讨论拆包装,我们看到了一个使用注释的项目的例子 @ 生成(从java.xml.ws.annotation模块)和 @ 非空(从JSR 305实现)。我们发现了三件事: -
两个注释都在 javax .annotation包中,因此创建了一个分割
-
java--add-modulesjava.xml.ws.annotation
--patch-module java.xml.ws.annotation=jsr305-3.0.2.jar
--class-path $dependencies
-jar $appjar
-
所有 jar 包都是非模块化的,运行命令为:java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter -
mod3 和 mod4 模块化后,运行命令为:java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar –add-modules mod3,mod4 mod1.EventCenter
-
mod2a、mod2b 的模块化后,运行命令为:java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar –add-modules mod2a,mod2b,mod4 mod1.EventCenter -
由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到 –add-modules 参数里了。 -
最后完成 mod1 的模块化改造,最终运行命令就简化为:java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter -
注意此时应用是以 -m 方式启动,并且指定了 mod1 为主模块(也是根模块),因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。
-
问题:自底向上策略很容易理解,实施路径也很清晰,但它存在一个有些模块无法进行模块化改造的问题。
-
思路:根据 jar 包依赖关系,从主应用开始,沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类: -
第一类:可以改造的,我们仍然采用自底向上策略进行改造,直至主应用完成改造; -
第二类:无法改造的,需要从一开始就放入模块路径,即转为自动模块。这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包,这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块,并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外,其它命名模块是不允许访问未命名模块的类),这样就保证自动模块自身可以照常访问其他类。等到主应用完成模块化改造,应用的启动方式就可以改为 -m 方式。
-
举例:还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造,那么最终改造完之后,虽然应用运行命令和之前一样还是java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造,借由模块系统转为自动模块。
-
不完美之处:看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况,如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块,要么自己动手丰衣足食 Hack 一个版本。当然,你也可以试试找到这些自动模块的维护者们,让他们 PK 一下决定谁才是这个分裂包的主人。
-
模块命名规范:给模块取一个有意义的名字,通常使用逆域名表示法(例如:com.example.myapp)。
-
明确的依赖关系:在module-info.java文件中明确声明模块的依赖关系,以确保应用程序的模块之间的依赖关系清晰可见。
-
最小依赖原则:尽量减少模块之间的依赖关系,只依赖于真正需要的模块。
-
版本化的依赖关系:如果可能的话,使用版本化的依赖关系来确保模块依赖的是正确的版本。
-
单一责任原则:将每个模块限制为一个特定的功能或领域,以提高可维护性和可重用性。
-
测试和验证:确保模块之间的依赖关系和交互在编译时和运行时都能正常工作。
-
模块路径管理:管理模块路径以确保应用程序能够正确加载和运行。
-
模块依赖关系:仔细考虑您的模块之间的依赖关系。确保模块之间的依赖关系是明确的,避免循环依赖。使用requires语句声明依赖关系,并根据需要使用requires transitive或requires static。
-
版本管理:了解模块之间的版本管理。Java 9引入了模块化版本的概念,允许模块依赖于特定版本的其他模块。考虑使用requires static来声明可选的、仅在特定版本下才有效的依赖关系。
-
模块命名:为您的模块选择合适的名称。模块名称应该唯一且易于理解。遵循Java的包命名约定,使用反向域名(例如com.example.mymodule)。
-
模块路径:在运行应用程序时,使用–module-path选项指定模块路径。确保正确设置模块路径,以便Java可以找到并加载您的模块。
-
非模块化库:如果您使用了非模块化的JAR文件,将其包装为自动模块或创建模块化的版本。非模块化库的依赖关系可能会引入复杂性。
-
模块化库:考虑使用已经模块化的库,以减少与模块路径和版本管理相关的问题。
-
运行时镜像:如果您使用jlink创建自定义运行时镜像,请确保包括了所有必要的模块,并排除不必要的模块,以减小应用程序的大小。
-
测试:编写单元测试以确保模块化应用程序的正确性。使用模块路径和–module选项来模拟模块化环境进行测试。
-
模块描述文件:模块描述文件(module-info.java)是模块化应用程序的关键组成部分。确保正确声明依赖关系、导出和打包模块,以及使用其他关键字来管理可见性。
-
模块间通信:模块之间的通信应该在依赖模块的基础上进行。不要尝试绕过模块系统的可见性控制。
-
跨模块访问:如果需要在模块之间共享数据或访问非公开成员,请使用opens和opens…to语句,以允许受信任的模块进行反射操作。
-
性能和内存开销:模块化应用程序的启动时间和内存开销可能会有所增加。在部署和测试应用程序时,要考虑性能方面的因素。
-
迁移:如果您正在迁移现有的应用程序到模块化架构,确保逐步迁移,以减少中断和问题。
-
文档和培训:为开发团队提供关于模块化的文档和培训,以确保所有开发人员都理解和遵守模块化的最佳实践。
-
工具支持:使用Java 9及更高版本,以充分利用模块化系统和相关的工具,如jdeps、jlink和jmod。