Java 模块解耦的设计策略
Java 平台模块系统 (JPMS) 提供了更强的封装、更高的可靠性和更好的关注点分离,有些同学可能没注意到。
不过呢,也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上,因此在许多情况下,模块彼此紧密耦合。
这可能会让我们认为模块化和松散耦合是不能在同一系统中共存的特性。不过呢,他们可以的!
接着,我们就来深入研究两种众所周知的设计模式,我们可以使用它们轻松解耦 Java 模块。
1、创建项目
我们弄个多模块的 Mavene 项目来演示吧。
为了保持代码简单,该项目最初将包含两个 Maven 模块,每个 Maven 模块将被包装到一个 Java 模块中。
第一个模块将包括一个服务接口以及两个实现——服务提供者。
第二个模块将使用提供程序来解析字符串值。
咱创建一个名为 Demoproject 的项目根目录,然后定义一下项目的父 POM:
<packaging>pom</packaging><modules><module>servicemodule</module><module>consumermodule</module>
</modules><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>11</source><target>11</target></configuration></plugin></plugins></pluginManagement>
</build>
2、服务模块
出于演示目的,让我们使用一种快速而肮脏的方法来实现 servicemodule 模块,这样我们就可以清楚地发现此设计中出现的缺陷。
让我们将服务接口和服务提供者公开,将它们放在同一个包中并全部导出。这似乎是一个相当不错的设计选择,但正如我们稍后将看到的,它极大地提高了项目模块之间的耦合程度。
在项目的根目录下,我们将创建 servicemodule/src/main/java 目录。然后,我们需要定义包 com.baeldung.servicemodule,并在其中放置以下 TextService 接口:
public interface TextService {String processText(String text);}
TextService 接口非常简单,接着我们定义服务提供者。
在同一个包中,我们添加一个 Lowercase 实现:
public class LowercaseTextService implements TextService {@Overridepublic String processText(String text) {return text.toLowerCase();}}
现在,让我们添加一个大写实现:
public class UppercaseTextService implements TextService {@Overridepublic String processText(String text) {return text.toUpperCase();}}
最后,在 servicemodule/src/main/java 目录下,包含模块描述符 module-info.java:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule;
}
3、消费者模块
现在我们需要创建一个使用我们之前创建的服务提供者之一的消费者模块。
让我们添加 com.baeldung.consumermodule.Application 类:
public class Application {public static void main(String args[]) {TextService textService = new LowercaseTextService();System.out.println(textService.processText("Hello from Baeldung!"));}
}
现在,让我们在源根目录中包含模块描述符 module-info.java,它应该是consumermodule/src/main/java:
module com.baeldung.consumermodule {requires com.baeldung.servicemodule;
}
最后,运行它。
但有一个值得注意的重要警告:我们不必要地将服务提供者耦合到消费者模块。
由于我们使提供者对外界可见,因此消费者模块能够意识到它们。
此外,这不利于使软件组件依赖于抽象。
4、服务提供者工厂
我们可以通过仅导出服务接口来轻松消除模块之间的耦合。相比之下,服务提供者不会被导出,因此对消费者模块来说仍然是隐藏的。消费者模块只能看到服务接口类型。
为了实现这一目标,我们需要:
1、将服务接口放在单独的包中,导出给外界
2、将服务提供者放在不同的包中,该包不导出
3、创建一个工厂类,并将其导出。消费者模块使用工厂类来查找服务提供者
我们可以将上述步骤概念化为设计模式的形式:公共服务接口、私有服务提供者和公共服务提供者工厂。
4.1 公共服务接口
为了清楚地了解此模式的工作原理,让我们将服务接口和服务提供者放在不同的包中。接口将被导出,但提供程序实现不会被导出。
因此,让我们将 TextService 移至一个名为 com.baeldung.servicemodule.external 的新包。
4.2 私服提供者
然后,我们同样将 LowercaseTextService 和 UppercaseTextService 移动到 com.baeldung.servicemodule.internal。
4.3 公共服务提供者工厂
由于服务提供者类现在是私有的,无法从其他模块访问,因此我们将使用公共工厂类来提供一种简单的机制,消费者模块可以使用该机制来获取服务提供者的实例。
在 com.baeldung.servicemodule.external 包中,我们定义 TextServiceFactory 类:
public class TextServiceFactory {private TextServiceFactory() {}public static TextService getTextService(String name) {return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();}}
当然,我们可以使工厂类稍微复杂一些。为了简单起见,服务提供者只是根据传递给 getTextService() 方法的字符串值创建的。
现在,让我们替换 module-info.java 文件以仅导出外部包:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule.external;
}
请注意,我们仅导出服务接口和工厂类。这些实现是私有的,因此它们对其他模块不可见。
4.4 应用类
现在,让我们重构 Application 类,以便它可以使用服务提供者工厂类:
public static void main(String args[]) {TextService textService = TextServiceFactory.getTextService("lowercase");System.out.println(textService.processText("Hello from Baeldung!"));
}
然后,运行它。
通过将服务接口设为公开,将服务提供者设为私有,我们可以通过简单的工厂类有效地解耦服务和消费者模块。
当然,没有任何模式是灵丹妙药。与往常一样,我们应该首先分析我们的用例是否适合。
5、服务和消费者模块
JPMS 通过provides…with 和uses 指令为开箱即用的服务和消费者模块提供支持。
因此,我们可以使用这个功能来解耦模块,而无需创建额外的工厂类。
为了让服务和消费者模块协同工作,我们需要执行以下操作:
1、将服务接口放在模块中,模块导出接口
2、将服务提供者放在另一个模块中 - 提供者被导出
3、在提供者的模块描述符中指定我们想要使用provides…with指令提供TextService实现
4、将 Application 类放置在它自己的模块中——消费者模块
5、在消费者模块的模块描述符中指定该模块是带有使用指令的消费者模块
6、使用消费者模块中的 Service Loader API 来查找服务提供者
这种方法非常强大,因为它利用了服务和消费者模块带来的所有功能。但这也有点棘手。
一方面,我们让消费者模块只依赖于服务接口,而不依赖于服务提供者。另一方面,我们甚至可以根本不定义服务提供者,应用程序仍然可以编译。
5.1 父模块
为了实现这个模式,我们还需要重构父 POM 和现有模块。
由于服务接口、服务提供者和消费者现在将位于不同的模块中,我们首先需要修改父 POM 的 部分,以反映这个新结构:
<modules><module>servicemodule</module><module>providermodule</module><module>consumermodule</module>
</modules>
5.2 服务模块
我们的 TextService 接口将返回 com.baeldung.servicemodule。
我们将相应地更改模块描述符:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule;
}
5.3 提供者模块
如前所述,提供程序模块用于我们的实现,因此现在让我们将 LowerCaseTextService 和 UppercaseTextService 放在这里。我们将它们放入一个名为 com.baeldung.providermodule 的包中。
最后,我们添加一个 module-info.java 文件:
module com.baeldung.providermodule {requires com.baeldung.servicemodule;provides com.baeldung.servicemodule.TextService with com.baeldung.providermodule.LowercaseTextService;
}
5.4 消费者模块
现在,让我们重构消费者模块。首先,我们将应用程序放回 com.baeldung.consumermodule 包中。
接下来,我们将重构 Application 类的 main() 方法,以便它可以使用 ServiceLoader 类来发现适当的实现:
public static void main(String[] args) {ServiceLoader<TextService> services = ServiceLoader.load(TextService.class);for (final TextService service: services) {System.out.println("The service " + service.getClass().getSimpleName() + " says: " + service.parseText("Hello from Baeldung!"));}
}
最后,我们将重构 module-info.java 文件:
module com.baeldung.consumermodule {requires com.baeldung.servicemodule;uses com.baeldung.servicemodule.TextService;
}
然后,运行它!
正如我们所看到的,实现这种模式比使用工厂类的模式稍微复杂一些。即便如此,额外的努力也会通过更灵活、松散耦合的设计得到高度回报。
费者模块依赖于抽象,并且在运行时也很容易插入不同的服务提供者。
6、总结
我们学习了如何实现两种模式来解耦 Java 模块。
这两种方法都使消费者模块依赖于抽象,这始终是软件组件设计中所需的功能。
当然,每一种都有其优点和缺点。对于第一个,我们得到了很好的解耦,但我们必须创建一个额外的工厂类。
对于第二个,为了使模块解耦,我们必须创建一个额外的抽象模块并使用 Service Loader API 添加新的间接级别。
相关文章:
Java 模块解耦的设计策略
Java 平台模块系统 (JPMS) 提供了更强的封装、更高的可靠性和更好的关注点分离,有些同学可能没注意到。 不过呢,也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上,因此在许多情况下,模块彼此紧密耦合…...

支持https访问
文章目录 1. 打开自己的云服务器的 80 和 443 端口2. 安装 nginx3. 安装 snapd4. 安装 certbot5. 生成证书6. 拷贝生成的证书到项目工作目录7. 修改 main.go 程序如下8. 编译程序9. 启动程序10. 使用 https 和端口 8081 访问页面成功11. 下面修改程序,支持 https 和…...

JavaScript 中常用简写技巧总结
平时我们写代码时最高级的境界是自己写的东西别人看不懂!哈哈哈!分享一些自己常用的js简写技巧,长期更新,会着重挑选一些实用的简写技巧,使自己的代码更简洁优雅~ 这里只会收集一些大多数人不知道的用法,但…...

第15集丨Vue 江湖 —— 组件
目录 一、为什么需要组件1.1 传统方式编写应用1.2 使用组件方式编写应用1.3 Vue的组件管理 二、Vue中的组件1.1 基本概念1.1.1 组件分类1.1.2 Vue中使用组件的三大步骤:1.1.3 如何定义一个组件1.1.4 如何注册组件1.1.5 如何使用组件 1.2 注意点1.2.1 关于组件名1.2.2 关于组件标…...

【JVM】CPU飙高排查方案与思路
文章目录 CPU飙高排查方案与思路 CPU飙高排查方案与思路 1.使用top命令查看占用cpu的情况 2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:40940 3.查看进程中的线程信息 4.可以根据进程 id 找到有问题的线程&a…...

使用公网访问内网IIS网站服务器【无需公网IP】
使用公网访问内网IIS网站服务器【无需公网IP】 文章目录 使用公网访问内网IIS网站服务器【无需公网IP】前言1. 注册并安装cpolar2. 创建隧道映射3. 获取公网地址 前言 这里介绍通过内网穿透,实现公网访问内网IIS网站服务器。 都知道,现在基本不会被分配…...

Vim学习(二)—— 编译C程序
打开终端,这里以MobaXterm为例, 邮件创建新的空文件并命名, 然后cd到对应路径下,用 vim hello.cvim打开创建的文件,进入编辑模式,编辑完程序后按Esc退出编辑模式,输入 :wq保存并退出…...
【maven】常见命令
文章目录 1. 打包编译时跳过测试2.显示maven依赖树3. 显示maven依赖列表4. 下载依赖包的源码5. 安装本地jar到本地仓库 1. 打包编译时跳过测试 mvn clean install -DskipTests mvn clean install -Dmaven.test.skiptrueDskipTests,不执行测试用例,但编译…...
vue单项数据传输流式回复功能,post传值可关闭请求(@microsoft/fetch-event-source)
需求:实现一个类似于文心一言ai回复功能,一个字一个字往外蹦,不使用websocket还有什么其他方案呢?经过查询有一个 microsoft/fetch-event-source单向传输协议(服务端传输客户端)。废话不多说,上…...
“深入探究JVM内部机制:理解Java虚拟机的工作原理“
标题:深入探究JVM内部机制:理解Java虚拟机的工作原理 摘要:本文将深入分析Java虚拟机(JVM)的工作原理,包括类加载、内存管理、垃圾回收和即时编译等方面。通过详细解释这些概念,并给出示例代码…...

ubuntu18.04下配置muduoC++11环境
1.安装muduo依赖的编译工具及库 Cmake sudo apt-get install cmakeBoost sudo apt-get install libboost-dev libboost-test-devcurl、c-ares DNS、google protobuf sudo apt-get install libcurl4-openssl-dev libc-ares-dev sudo apt-get install protobuf-compiler libp…...

leetcode 力扣刷题 数组交集(数组、set、map都可实现哈希表)
数组交集 349. 两个数组的交集排序+双指针数组实现哈希表unordered_setunordered_map 350. 两个数组的交集Ⅱ排序 双指针数组实现哈希表unordered_map 349. 两个数组的交集 题目链接:349. 两个数组的交集 题目内容如下,理解题意:…...

JVM元空间溢出的排除思路
背景: java的应用我们为了防止元空间的无限扩展,一般都会设置MaxMetaSpace参数,一般来说只要这个值是512M或者1G左右就足够了,不过今天遇到一个meta空间溢出问题,简单记录下排除的思路 meta元空间溢出 最开始的现象…...
vue+java实现在线播放mp4视频
java: 读取本地视频文件的流然后给response的输出流 File file new File("/Users/zhangqingtian/Documents/水库/Floodforecast/static/" videoName);BufferedInputStream inputStream new BufferedInputStream(new FileInputStream(file));response.setContentT…...
手机两个卡槽的正确使用方法,您用对了吗?
手机上有两个卡槽,该如何搭配才能使话费降到最低?你又是怎么搭配的? 这篇文章小编就来告诉你,如何在不换号的情况下,将自己的话费降到最低。 首先卡槽一我们就用8元保号套餐。 卡槽二,我们就可以办理一张…...

PyTorch翻译官网教程-NLP FROM SCRATCH: CLASSIFYING NAMES WITH A CHARACTER-LEVEL RNN
官网链接 NLP From Scratch: Classifying Names with a Character-Level RNN — PyTorch Tutorials 2.0.1cu117 documentation 使用CHARACTER-LEVEL RNN 对名字分类 我们将建立和训练一个基本的字符级递归神经网络(RNN)来分类单词。本教程以及另外两个“from scratch”的自然…...

基于注意力神经网络的深度强化学习探索方法:ARiADNE
ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration 文章目录 ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration机器人自主探索(ARE)ARE的传统边界法非短视路径深度强化学习的方…...

Martin_DHCP_V3.0 (DHCP自动化泛洪攻击GUI)
Github>https://github.com/MartinxMax/Martin_DHCP_V3.0 首页 Martin_DHCP_V3.0 自动化DHCP洪泛攻击 Martin_DHCP_V3.0 使用方法 安装三方库 #python3 1.RunMe_Install_Packet.py 攻击路由器 #python3 Martin_DHCP_Attack.py 填写网卡 填写攻击次数 开始运行...

vscode vue3+vite 配置eslint
vue2webpackeslint配置 目前主流项目都在使用vue3vite,因此针对eslint的配置做了一下总结。 引入ESlint、pritter 安装插件,执行以下命令 // eslint // prettier // eslint-plugin-vue // eslint-config-prettier // eslint-plugin-prettier yarn ad…...

【C++学习手札】一文带你初识运算符重载
食用指南:本文在有C基础的情况下食用更佳 🍀本文前置知识: C类 ♈️今日夜电波:クリームソーダとシャンデリア—Edo_Ame江户糖 1:20 ━━━━━━️💟──────── 3:40 …...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
Python ROS2【机器人中间件框架】 简介
销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...

代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...

rknn toolkit2搭建和推理
安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 ,不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源(最常用) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...