Java 模块化开发

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Java 模块化开发

前言

       之前在 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

总结

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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
安全 Java API
Java一分钟之——Java模块系统:模块化开发(Jigsaw)
【5月更文挑战第20天】Java 9引入了Jigsaw模块系统,改善代码组织和依赖管理。模块通过`module-info.java`定义,声明名称、导出包及依赖。常见问题包括依赖循环、未声明依赖和过度导出。避免这些问题的策略包括明确声明依赖、谨慎导出包和避免循环依赖。通过实例展示了模块间的关系,强调理解模块系统对于构建整洁、安全和可维护的Java应用的重要性。
116 5
|
6月前
|
Java 大数据 云计算
Java未来展望:从Java 17到模块化开发的新时代
【2月更文挑战第12天】 随着技术的不断进步和软件开发需求的不断增长,Java作为一种历史悠久且广泛使用的编程语言,其发展方向和新特性备受关注。本文首先回顾了Java的发展历程,特别是Java 17版本的重要更新,然后深入探讨了Java在模块化开发方面的进展和挑战,以及这些变化对开发者社区和软件工程实践可能产生的影响。文章旨在为读者提供对Java未来发展趋势的洞察,特别是在模块化开发领域的应用前景。
|
Java API 算法框架/工具
阿里再开源!基于JAVA的模块化开发框架JarsLink
JarsLink是一个基于JAVA的模块化开发框架,它提供在运行时动态加载模块(JAR包)、卸载模块和模块间调用的API,它能够帮助你进行模块化开发,也能帮助你的系统在运行时动态添加新功能,减少编译、打包和部署带来的发布耗时,同时它也是阿里巴巴的开源项目之一,目前在蚂蚁金服微贷事业群各团队广泛使用。
7401 0
阿里再开源!基于JAVA的模块化开发框架JarsLink
|
8天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
17天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
23 9
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin