Java 模块化开发

news2024/11/18 2:33:31

前言

        之前在 Github 下载的好多代码发现都是 Java 模块化开发出来的,模块化是 JDK9 引入的,所以在 JDK9 及其后续的版本中,都可以采用模块化开发的方法来进行项目的开发。尤其是Java桌面应用开发,虽然这只是我的一个业余爱好,但是多学点技术没什么坏处。

1、Java 模块化开发

1.1、概述

        在 JDK9 之前,无论是运行一个大型的软件系统,还是运行一个小的程序,即使程序只需要使用Java的部分核心功能, JVM也要加载整个JRE环境。所以为了解决这个问题,让Java实现轻量化,Java 9正式的推出了模块化系统。Java被拆分为N多个模块,并允许Java程序可以选择的加载模块,这样就可以让Java以轻量化的方式来运行。

        Java 模块化对于开发桌面软件的好处是非常大的,之前写一个不管多简单的 JavaFX 应用,要想在所有没有 Java 环境的机器上运行必须把 JRE 也打包到软件里面,但是有了模块化,我们只需要把用到的类库打包即可。

1.2、模块的引入和模块内包的导出

        模块化最鲜明的特征就是需要在代码目录下创建一个名为 "module-info.java" 的模块描述文件,这个文件定义了该模块需要引用的其它模块、暴露模块中的哪些包给外部哪些模块、提供给外部模块哪些接口服务、使用了其它模块的哪些接口服务等。

1.2.1、模块的导出

下面我们新建一个 Maven 项目,先创建三个模块:

在 module1 下的 com.lyh.domain 包下创建一个类 User:

package com.lyh.domain;

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User(){
    }

    public void sayHello(){
        System.out.println("Hello " + name);
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

1.2.2、模块的导入 

现在我们希望在模块2中可以调用它,所以我们需要在 module1 下的 java 目录创建 module-info.java:

module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B; // 用 to 指定只能给 B模块使用
}

        这里我们定义 module1 的模块名为 A,这其实是不影响的,只要保证我们的项目中没有重名的就可以。现在我们希望 module2 可以使用 module1 中的 User 类,所以我们需要 module1 暴露它的 com.lyh.domain 包。至于上面的 require java.base 其实是每个模块默认都会导入的包,我们也可以省略。

        这里还需要说明的是,默认暴露的包都是允许其它模块通过反射来访问该包下用 public 修饰的内容的

module1 的模块描述文件配置完毕之后,我们需要配置 module2 的:

module B {
    // 引入A模块暴露的包并传递依赖
    requires transitive A;
}

这里可以通过 Idea 快捷键,也可以通过 Project Structure 来设置: 

 

        module2 这里我们同样设置它的模块名为 B ,然后我们引入了 A 模块,但这样并不是说我们就可以访问模块 A 中所有的包了,我们只可以访问模块 A 给我们暴露出来的包!

 测试

package com.lyh;

public class App {
    public static void main(String[] args) {
        new User("李大国",25).sayHello();
    }
}

 运行结果:

 1.2.3、模块的依赖传递

        "依赖传递" 这个名字是我自己起的,意思是说如果我的模块B 引用了模块 A ,而现在我的模块C 引用了模块B ,那么我的模块C 在调用模块B 时,模块B 调用了A 的方法是否可行呢?毕竟模块C 并没有引用模块A 。

        答案是不可行的,但是只需要在模块B 引入模块A 的位置加上一个关键字 transitiive 即可实现依赖的传递,这样之后所有引用了模块B 的模块就不用担心模块B 引用了其它的依赖而自己的模块内部却没有引用而出现问题了:

module B {
    // 引入A模块暴露的包并传递依赖
    requires transitive A;
}

 1.3、模块内反射访问控制语法

        关于反射,普通程序员一般用的并不多,只有写框架的人用才会经常用,但是这里依然需要了解一下。

上面我们说:默认暴露的包都是允许其它模块通过反射来访问该包下用 public 修饰的内容的。其实,我们还可以通过 opens 语法来定义可以通过反射来访问该包下任意修饰符修饰的内容。

1.3.1、opens  语法

        现在,我们试着通过 opens 语法来让模块C(module3) 来通过反射调用 User 的 sayHello 方法:

 首先修改模块A 的模块描述文件:

module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B,C; // 用 to 指定只能给 B模块使用

    // 允许外部模块反射的包,即使是非 public 也是可以访问到的
    opens com.lyh.domain to C; // 同样可以指定只能给特定模块访问,多个模块之间用逗号隔开
}

然后创建module3 的模块配置文件,并引入模块A 的包:

module C {
    requires A;
}

在 module3 中测试: 

package com.lyh.run;

import java.lang.reflect.Method;

public class App {
    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("com.lyh.domain.User");
            System.out.println(c.getName());

            Method sayHello = c.getDeclaredMethod("sayHello");
            sayHello.setAccessible(true);
            sayHello.invoke(c.getConstructor().newInstance(),null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

 1.3.2、open 语法

        除了使用 opens,我们还可以直接在模块名前面直接来一个 open 关键字来定义整个模块暴露的包都允许外部模块通过反射使用:

open module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B,C; // 用 to 指定只能给 B模块使用
}

注意:使用了 open ,我们的模块描述文件中就不能再使用 opens 了,不然会报错。

1.4、模块服务

        问:模块是如何向外界提供服务的?

        答:模块只向外界公开接口,具体的实现类是不公开的,这就实现了模块之间的低耦合,比如我们的模块A有一个接口用来提供数据库服务,模块2需要调用这个接口来获得服务。那么无论模块A中的服务发生怎样的变化,我们的模块B不需要任何改变去适应这种变化。

        下面我们来模拟一下,我们首先在模块A上定义一个接口用来提供数据库服务:

package com.lyh.service;

public interface DBService {
    void connect();
}

        然后我们需要提供两个实现类,分别代表连接 MySQL 和 Oracle 数据库的服务:

package com.lyh.impl;

import com.lyh.service.DBService;

public class MySqlServiceImpl implements DBService {
    @Override
    public void connect() {
        System.out.println("连接 MySQL");
    }
}
package com.lyh.impl;

import com.lyh.service.DBService;

public class OracleServiceImpl implements DBService {
    @Override
    public void connect() {
        System.out.println("连接 Oracle");
    }
}

 然后,我们需要向外部模块提供该服务:通过在 module-info.java 中声明提供的服务:

open module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B,C; // 用 to 指定只能给 B模块使用

    exports com.lyh.service;
    provides com.lyh.service.DBService with com.lyh.impl.MySqlServiceImpl;

}

        我们首先需要暴露接口所在的包,以便外部模块可以访问;然后我们用 provides 语法来声明提供的服务接口和实现类(注意:这里必须指定接口的实现类)。

现在我们可以在模块B 中去调用模块A 的服务了:
        首先,我们需要引入A 模块并通过 use 语法来使用 A模块提供的服务:

module B {
    
    requires A;

    uses com.lyh.service.DBService;
}

测试: 

package com.lyh;

import com.lyh.service.DBService;
import java.util.ServiceLoader;

public class App {
    public static void main(String[] args) {
        ServiceLoader<DBService> dbServices = ServiceLoader.load(DBService.class);
        for(DBService service: dbServices)
            service.connect();
    }
}

运行结果:

        这样,之后如果我们的模块A 希望修改服务为 Oracle 服务,只需要在 module-info.java 中修改即可:

open module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B,C; // 用 to 指定只能给 B模块使用

    exports com.lyh.service;
    provides com.lyh.service.DBService with com.lyh.impl.MySqlServiceImpl,com.lyh.impl.OracleServiceImpl;

}

        当然,这里的接口不只可以提供一个服务,如果有多个服务的需求,只需要通过逗号把该接口的实现类分割开来即可: 

open module A {
    // java.base 默认就已经导入了,这里可以省略
    requires java.base;
    // 暴露包给外部模块使用
    // 导出的包默认是允许反射访问 public 修饰的包的
    exports com.lyh.domain to B,C; // 用 to 指定只能给 B模块使用

    exports com.lyh.service;
    provides com.lyh.service.DBService with com.lyh.impl.OracleServiceImpl;

}

测试运行:

1.5、模块相关命令行使用

1.5.1、查看 JDK 中所有模块

java --list-modules

1.5.2、查看模块详细信息

java --describe-module java.xml

总结

        至此,模块化的知识点基本上完了,之后在开发过程中遇到什么问题再来更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1594588.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WindowsServer 2022 AD域控-006-安装副域控

试验拓扑图&#xff1a; 一、测试单域控故障&#xff0c;用户无法修改密码&#xff1b; 域控断网&#xff0c;Win10测试; 二、WindowsServer2022 DC02加入域控&#xff1b; 加入成功 此时域控上只有DC02这台服务器&#xff0c;但DC02并不是域控&#xff1b; 三、WindowsS…

防汛物资仓库管理系统|实现应急物资仓库三维可视化

系统概述 智慧应急物资仓库可视化系统&#xff08;智物资DW-S300&#xff09;采用了 B/S 架构的设计&#xff0c;通过浏览器即可快速登录操作。实现对库房内的应急物资从申购入库、出库、调拨、库内环境监测、维修保养、检测试验、处置报废等全周期、科学、规范的管理。系统以…

使用DockerCompose安装Redis

本文使用docker-compose的方式安装Redis&#xff0c;如何未安装docker-compose&#xff0c;可以参考这篇文章进行安装【在Ubuntu上安装Docker Compose】 一、创建一个DockerCompose配置文件 第一步&#xff1a;创建相关目录文件 为了更好的组织管理Docker容器的配置文件和映射…

python linux服务器ssh简单爆破(测试用户名密码)(连接ssh服务器)(测试登录ssh服务器)

文章目录 背景示例代码代码解释导入模块SSH服务器的地址和端口用户名和密码列表生成所有可能的用户名和密码组合尝试连接到SSH服务器并验证用户名和密码遍历并测试每一对凭证 背景 我们华为摄像头linux终端的密码忘了&#xff0c;还不太好初始化&#xff0c;手动一个个测试太麻…

每日一题——阶乘计算升级版

题目链接&#xff1a; 6-10 阶乘计算升级版 - 基础编程题目集 (pintia.cn) 题目&#xff1a; 6-10 阶乘计算升级版 分数 20 本题要求实现一个打印非负整数阶乘的函数。 函数接口定义&#xff1a; void Print_Factorial ( const int N ); 其中N是用户传入的参数&#xff…

Nacos的安装(windows环境下)

1. 下载Nacos安装包 点击下载安装包 将安装包解压到本地目录下&#xff08;路径中不要带中文&#xff09; 修改配置文件 修改为本地M有SQL的用户名和密码&#xff1a; 启动Nacos 1&#xff09;打开nacos文件所在的bin目录下 2&#xff09;用cmd命令行窗口打开在bin目录下 …

Kubernetes的Ingress Controller

前言 Kubernetes暴露服务的方式有一下几种&#xff1a;LoadBlancer Service、ExternalName、NodePort Service、Ingress&#xff0c;使用四层负载均衡调度器Service时&#xff0c;当客户端访问kubernetes集群内部的应用时&#xff0c;数据包的走向如下面流程所示&#xff1a;C…

JS/TS笔记学习1

周末总得学点什么吧~ 奥利给! 跑火车 递归 减速 let currentIndex 0; let speed 500; // 初始速度&#xff0c;单位是毫秒 let decrement 20; // 每次迭代速度减少的量 const cells document.querySelectorAll(.cell); function highlightCell() { cells.forEach(…

vue3 uniapp微信登录

根据最新的微信小程序官方的规定&#xff0c;uniapp中的uni.getUserInfo方法不再返回用户头像和昵称、以及手机号 首先&#xff0c;需获取appID&#xff0c;appSecret&#xff0c;如下图 先调用uni.getUserInfo方法获取code&#xff0c;然后调用后台的api&#xff0c;传入code&…

AI禁区徘徊监测识别摄像机

AI禁区徘徊监测识别摄像机是一种基于人工智能技术的智能监控设备&#xff0c;用于监测禁止进入或逗留的区域。这种摄像机通过高清摄像头实时捕捉场景图像&#xff0c;利用AI算法对人员徘徊行为进行识别和监测&#xff0c;有助于提高安全防范水平&#xff0c;减少潜在的安全风险…

免费分享Springboot+Vue的影院管理系统源码,真酷!

今天给大家分享一套基于SpringbootVue的影院管理系统源码&#xff0c;在实际项目中可以直接复用。(免费提供&#xff0c;文末自取) 一、系统运行图 1、登陆页面 2、系统后台 3、选座功能 影院管理系统通常具有以下七个功能点&#xff1a; 1.电影管理&#xff1a; 包括电影信…

CMC学习系列 (8):动态力输出期间的伽马范围皮质相干性

CMC学习系列:动态力输出期间的伽马范围皮质相干性 0. 引言1. 主要贡献2. 方法3. 结果4. 讨论5. 总结欢迎来稿 论文地址&#xff1a;https://www.sciencedirect.com/science/article/abs/pii/S1053811906010238 论文题目&#xff1a;Gamma-range corticomuscular coherence duri…

ARM v8 Cortex R52内核 04 时钟和复位 Clocking and Resets

ARM v8 Cortex R52内核 04 时钟和复位 Clocking and Resets 4.1 Clock and clock enables 时钟和时钟使能 Cortex-R52处理器具有一个单一的时钟&#xff0c;驱动着所有的触发器和RAM。各种输入&#xff0c;包括复位输入&#xff0c;都有同步逻辑使它们可以与处理器时钟异步操…

C语言 | Leetcode C语言题解之第20题有效的括号

题目&#xff1a; 题解&#xff1a; char pairs(char a) {if (a }) return {;if (a ]) return [;if (a )) return (;return 0; }bool isValid(char* s) {int n strlen(s);if (n % 2 1) {return false;}int stk[n 1], top 0;for (int i 0; i < n; i) {char ch pair…

模型训练-保存训练数据

1.目的 找到一个可运行的代码&#xff0c;可以每个epoch打印训练数据&#xff0c;但是不会保存。因为在改进模型需要这些训练数据进行对比&#xff0c;所以需要将每个epoch的训练数据保存下来&#xff0c;写到一个文件中。 2.解决方案1 直接问ChatGPT&#xff0c;提示词如下…

5.4Python之可变类型与列表的深浅拷贝

【1】可变类型与不可变类型 在Python中&#xff0c;数据类型可以分为可变类型&#xff08;Mutable&#xff09;和不可变类型&#xff08;Immutable&#xff09;。这指的是对象在创建后是否可以更改其值或状态。 不可变类型是指创建后不能更改其值或状态的对象。如果对不可变类…

二极管分类及用途

二极管分类及用途 通用开关二极管 特点&#xff1a;电流小&#xff0c;工作频率高 选型依据&#xff1a;正向电流、正向压降、功耗&#xff0c;反向最大电压&#xff0c;反向恢复时间&#xff0c;封装等 类型&#xff1a;BAS316 ; IN4148WS 应用电路: 说明&#xff1a;应用…

单路高清HDMI编码器JR-3211HD

产品简介&#xff1a; JR-3211HD单路高清HDMI编码器是专业的高清音视频编码产品&#xff0c;该产品具有支持1路高清HDMI音视频采集功能&#xff0c; 1路3.5MM独立外接音频输入&#xff0c;编码输出双码流H.264格式&#xff0c;音频MP3/AAC格式。编码码率可调&#xff0c;画面质…

双写一致性问题

双写一致性问题&#xff1a;同一份数据&#xff0c;需要写数据库、写缓存。数据库中的数据和缓存中的数据要一致 解决办法&#xff1a;延迟双删 当我们要进行更新操作时&#xff0c;先删除缓存&#xff0c;再更新数据库&#xff0c;延迟几百ms再删除一次redis的缓存数据。 示…

Java-Scanner类进阶+题目

Scanner进阶 接收整数数据时&#xff1a; 接收小数数据时&#xff1a; 例子&#xff1a; 可以先这样弄出scanner的框架&#xff1a; 未完待续... ...