Java 9 模块化系统详解

一、简介

1. 引入模块化系统原因

Java在发展过程中逐渐变得庞大而复杂,Java应用程序的规模逐渐变大,尤其是企业级应用代码结构臃肿不断添加自定义库或第三方依赖,这样会导致潜在的问题,例如包访问冲突和兼容性影响等。因此Java SE 9 引入了一种新的模块化系统来解决这些问题。

2. 模块化系统带来的优势和挑战

模块化可以更好地控制Java项目中使用的库,将一个完整的应用程序分解成合理的模块,可以消除在代码方面缺乏结构和组织性的问题,使编码更加模块化、灵活和可维护。另一方面模块化系统也会带来一些挑战,例如要花费额外的精力来处理模块之间的依赖关系以及必须要重新设计和组织项目结构中的源代码。

3. 模块化关键概念

  • 模块(Module):Java 模块是一个可以独立编译和部署的程序单元。模块是由包(Package)组成的,包含了该模块提供给外部的 API,也包含了该模块内部实现所需要的代码和数据。每个模块带有一个模块描述符,指定了模块的名称、版本、输入和输出。

  • 模块路径(Module Path):模块路径用于识别模块的引用,就像类路径用于识别类的引用一样。通过将模块添加到模块路径中,Java 运行时可以找到它们并使它们可用。

  • 需求(Requirement):需求是指一个模块依赖于另一个模块。模块描述符中定义了模块的需求列表,模块对其他模块的依赖是使用 require 语句进行声明的。

  • 导出(Export):导出指明了模块提供的 API 包,其他模块可以使用这些 API。如果一个模块没有声明 API(即没有导出),则其他模块无法使用该模块的 API。

  • 打包(Packaging):打包是指将模块的源代码或编译后的字节码存储成一个模块文件,以便部署。

二、模块化基础

1. 模块化源代码结构规范

Java SE 9 明确了使用模块的源代码结构规定,相比传统的Java应用程序它们的代码结构变得更加整洁。Java 的模块化源代码结构规范具体如下:

src/
   <module name>/
      module-info.java
      package1/
         ...
      package2/
         ...

模块源代码存储在名为“src”目录下,每个模块有自己的目录,在这个目录下每个文件包含了特定的软件组件。其中使用 module_info.java 文件来代表此模块。

2. 模块定义与描述符

在 Java 模块中模块描述符表示了模块对其他模块的依赖、导出等信息,它是指定用于组合和部署模块化应用程序的元数据文件。有关模块中描述符的细节介绍可以参考下面的代码示例

模块定义与描述符示例:

module com.mycompany.mymodule {
    
    
   requires org.slf4j;
   exports com.mycompany.mymodule.api;
}

模块描述符由一系列语句和指令组成,以空格、制表符、换行符作为分隔符进行书写。以上面的示例代码为例说明以下重要概念:

  • module:定义模块。模块名称与文件系统上该模块所属的目录名称是一致的

  • requires:声明一个模块依赖org.slf4j 是其依赖的模块名称。

  • exports:公开包。导出的包对外公布的类、接口将可以在其他模块中使用,如果不明确导出在 modules 中是不能调用的。

3. 打包可执行模块

在 Java SE 9 中,每个模块需要先编译成一组字节码,然后将其打包到一个单独的 JAR 文件中。可以使用Java平台中提供的 jlink 命令将模块链接起来并创建自定义运行时映像,以此打包每个模块中的所有类和依赖。这里给出一个简单示例:

javac -d mods/com.mycompany.mymodule \
   src/com.mycompany.mymodule/module-info.java \
   src/com.mycompany.mymodule/com/mycompany/mymodule/MyClass.java

打包结果:

├── mods
│   └── com.mycompany.mymodule-1.0.jar
└── src
    └── com.mycompany.mymodule
        ├── com
        │   └── mycompany
        │       └── mymodule
        │           └── MyClass.java
        └── module-info.java

三、模块化系统的高级特性

1. 模块发现与解决依赖

通过指定相应的 Requires 表达式,让别的模块去满足我们的模块要求所需要的依赖关系,Java SE 9 支持两种形式的依赖关系,分别为“强”与“传递”。

链接多个模块的集合被称为运行时图(Runtime图),运行时图的创建是通过实现模块之间的所有 Require 表达式来完成的。

2. 模块化升级与替换

Java SE9 提供了“模块可见性”这一高级特性,可以根据具体需求隐藏某些模块,防止在模块升级和替换时出现不必要的访问和引用。使用方式如下:

在 Java 配置文件 (java.conf) 中,修改模块路径为:

modulepath=/usr/java/jdk-9.0.1/conf/runtime-image
# hidden:
//ignored module notfound.module
# opened /opt/found/bin to foo.bar

在编译时添加 -Xmodule:module/name 参数

3. 模块化动态访问

JEP 261 提供了一种新的方式,使得从另一个模块中加载类变得尽可能简单。应用程序可以请求访问其它模块的非公共类、接口和枚举,而无需将这些包或类全部导出为 public API。这种新的方式支持从程序内部动态增加、删除和重定位模块。此外Java 9 引入了一系列API,以便在运行时获取关于模块化自身的元数据信息,如 package\ class,及其他模块的描述拉GTE信息等。

一个简单的动态访问模块的示例:

// 动态获取模块属性
Module module = Class.forName("com.example.MyClass").getModule();
System.out.println(module.getName());

四、Java模块化实践

如何创建模块

Java 9 引入了模块化系统使得我们可以更好地控制代码的可见性、隔离性和依赖性。下面是创建模块的详细步骤:

步骤1:创建module-info.java文件

在Java 9中可以使用 module-info.java 文件定义一个模块及其依赖关系。只需要在项目根目录下创建一个名为 module-info.java 的文件,并编写以下内容即可:

module com.example.myapp {
    
    
    requires java.base;
    exports com.example.mypackage;
}
  • module 表示声明一个新的模块。
  • com.example.myapp 是该模块的名称。
  • requires java.base 表示该模块需要依赖于 Java 的基本模块。
  • exports com.example.mypackage 表示该模块对外暴露的包。如果有多个包需要对外暴露,可以使用逗号分隔。

步骤2:将项目转换成一个模块

要将现有项目转换为一个模块需要在编译时增加参数 --module-path--module。例如:

javac --module-path mods/ -d out/ src/com/example/mypackage/MyClass.java src/module-info.java
  • --module-path 指定模块路径,该路径应该是一个目录或 jar 文件。
  • -d 指定编译输出的目录。
  • src/module-info.java 是我们创建的 module-info.java 文件的路径。
  • src/com/example/mypackage/MyClass.java 是应用程序代码的路径。

步骤3:运行程序

要运行一个模块化的程序必须使用 --module 参数指定要运行的模块,例如:

java --module-path mods/ -m com.example.myapp/com.example.mypackage.MyClass

这条命令会运行 com.example.myapp 这个模块中的名为 com.example.mypackage.MyClass 的主类。

如何使用其他模块

在一个模块中使用其他模块非常简单,只需要在模块描述文件中声明这个模块即可。

  1. 声明依赖关系

在 module-info.java 文件中使用 requires 关键字声明依赖关系,例如:

module com.example.myapp {
    
    
    requires com.example.mymodule;
}
  1. 将外部模块添加到模块路径

使用 --module-path 参数将外部模块添加到模块路径中,例如:

java --module-path mods/ -m com.example.myapp/com.example.mypackage.MyClass

其中 mods/ 目录保存了所有需要使用的外部模块。如果需要加载多个模块,则可以使用冒号分隔每个模块的路径,例如:

java --module-path mods1/:mods2/ -m com.example.myapp/com.example.mypackage.MyClass

如何向后兼容

Java 模块化系统非常注重向后兼容。模块化应用程序可以像传统的 Java 应用程序一样运行在非模块化的环境中。

具体来说如果不提供 module-info.java 文件,则编译器会自动将我们的代码封装到一个自动模块中。自动模块的名称是基于 jar 文件的名称推断出来的,并且它依赖于所有其它的模块。

此外在使用传统的类路径方式运行模块化应用程序时,Java 运行时会将整个类路径看作单个模块。这个模块的名称是 unnamed module,并且它依赖于所有其他的模块。

五、模块化与类路径

1. 模块化系统与传统类路径系统的区别

  • 模块化系统通过定义明确的依赖关系来提高可靠性和安全性,并使代码更易于维护。
  • 传统类路径系统的依赖关系通常是隐式的,因此在大型应用程序中很容易出现混乱和冲突。

2. 模块路径与类路径的混合使用

当在同一个应用程序中混合使用模块路径和类路径时,应该注意以下两点:

  • 类路径中的类无法访问模块路径中的类。如果需要在类路径中使用模块中的类,可以将模块导出到一个 jar 文件中,然后将该 jar 文件添加到类路径中。
  • 在 Java 11 之前,classpath 参数被解释为类路径,但从 Java 11 开始,它被解释为模块路径。

六、模块化系统尚未解决的问题以及对于代码库的选项

1. Automatic Module Name

当我们在模块化应用程序中使用非模块化的库时,我们需要指定该库的模块名称,这就是 Automatic Module Name。

Automatic Module Name 是 Java 9 中引入的一个特性,它允许为非模块化的 Jar 包提供一个模块名称。

具体来说如果 jar 文件没有 module-info.class 文件,则它可以通过在 MANIFEST.MF 文件中添加一个名为 Automatic-Module-Name 的属性来设置模块名称。例如:

Automatic-Module-Name: mylibrary

这将使得我们可以像依赖于其他模块一样依赖于这个自动模块。

2. JBoss Modules 和 OSGi

虽然 Java 9 引入了官方的模块化系统,但 JBoss Modules 和 OSGi 这两种传统的模块化系统仍然在某些场合下非常有用。

JBoss Modules 是一个模块化系统,它是由 JBoss 作为其应用服务器的一部分开发的。与 Java 9 的模块化系统相比,JBoss Modules 更为灵活,允许多个版本的同一个库同时存在,并且支持动态加载和卸载模块。

OSGi 是一种更加通用的模块化系统,它可以在任何 Java 环境中使用。与 Java 9 的模块化系统相比,OSGi 具有更高的动态性和扩展性。

3. 微服务下的模块化

在微服务架构中,模块化非常重要。每个微服务都应该具有独立的、可重用的模块,并且这些模块应该很容易地被其他微服务使用。

Java 模块化系统为我们提供了一种良好的方式来组织和保护微服务代码库,使其更具可重用性和可维护性。如果您正在构建微服务应用程序,那么模块化应该是您的首选方案。

七、模块化系统的迁移指南和最佳实践

1. 迁移指南

要迁移到 Java 9 模块化系统,需要完成以下步骤:

  1. 确认项目是否具有明确定义的模块界限。
  2. 在所有 Java 源代码根目录下创建一个 module-info.java 文件。
  3. 在 module-info.java 文件中声明模块依赖关系。
  4. 更新构建脚本以在编译时使用新的命令行选项。
  5. 将应用程序的所有外部库转换为 Java 模块(可能需要创建自动模块名称)。
  6. 使用最新版本的 JDK 运行应用程序。

2. 实践经验

在实践中可以遵循以下最佳实践:

  • 定义一个清晰的目录结构,每个模块都有自己的代码仓库、测试文件夹和文档。
  • 将每个模块的界限设计为尽可能小而精细的单元。这样可以更容易地维护代码并提高可重用性。
  • 尽量使用对外不可见的包。例如,在一个模块中只需将包导出给其他互相关联的模块。
  • 尝试避免使用反射功能,因为这可能会破坏模块的可见性规则。

猜你喜欢

转载自blog.csdn.net/u010349629/article/details/130877520