JDK模块化之模块的基础概念

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK模块化之模块的基础概念相关的知识,希望对你有一定的参考价值。

JDK模块化二之模块的基础概念

文章目录

模块的基本定义、内容和配置

模块是代码、数据和资源的集合。它是一组包含代码、数据文件和一些静态资源的相关包和类型(类、抽象类、接口等)。

每个模块只包含一组相关的代码和数据,以支持单一责任原则(Single Responsibility Principle, SRP):“一个类更改的原因不应该超过一个。”(这个想法是设计一个只有一个职责的类。)用更简单的术语来说:模块=代码+数据。

Java 9模块系统的主要目标是支持用Java进行模块化编程。

现在,让我们研究基本模块和依赖关系,然后讨论增强封装的模块机制。

基础模块

目前,Java 9模块系统大约有98个模块,但它仍在继续发展。Oracle将JDK jar和Java SE规范分为两组模块。

所有JDK和用户定义模块的默认模块是基础模块java.base。它是一个独立的模块,不依赖于任何其他模块。java.base被称为“Java 9模块之母”。

图解:模块间的依赖关系

在下面的图中,可以看到系统的模块化方面,并且可以帮助理解模块化的JDK,意味着您也可以模块化您自己的应用程序。该图是一个依赖图。每个框表示一个模块,如果一个模块有一个指向另一个模块的箭头,这意味着箭头所在的模块需要它所指向的模块来执行其功能。

这只是98个平台模块中的一小部分

可以使依赖关系更显式:

这个图变得相当混乱,所以通常省略从模块到java.base的依赖关系。但重要的是要记住对java.base的依赖总是存在的。

在下面的图表中 java.loggingjava.xml模块在图中仍然有一个显式箭头。这是为了表明java.base是模块java.loggingjava.xml的唯一依赖项。

您可以看到一个具有java.base以外的依赖项的模块。

java.sql模块使用java.logging模块(这是有意义的,因为它可能有一些内部日志要做)和java.xml模块(这可能有点令人惊讶,但它的存在是为了处理一些数据库的XML功能)。

通过此图中的模块及其显式依赖关系,比在该语言的以前版本中查看rt.jar文件时了解各个功能之间的关系更直观清晰。

您可以清楚地了解JDK中不同的功能是如何打包的,提供功能的不同模块之间的依赖关系明显存在于何处。

将这些信息显式编码到模块(带有依赖项)中,可以更容易地创建可靠的应用程序,特别是在应用程序开发中应用这些相同的原则时。您新的、更模块化的应用程序将像这个图一样容易理解。

更好的封装实现和定义良好的接口

显式依赖是模块化的主要基础之一,但您还需要更强的封装形式和定义良好的接口,因此让我们放大单个模块,看看JPMS如何处理这些问题。

您看到的是一个单独的模块,母模块java.base,正如你所见,它有两个部分:

上半部分列出的包。在本例中,使用 java.lang, java.util, java.io (但是在真正的java.base模块中还有更多)。这些包都是这个模块的公共接口的一部分。每个模块都依赖java.base能看到这些包裹里的所有东西。

在模块的屏蔽部分的下面,你会看到不同的包,它们的名字像sun.utiljdk.internal。这是一个信号,表明这些包是内部实现细节,而依赖于java.base的其他模块将不能访问这些包中的任何内容。

这是模块系统提供的强封装(一个关键的安全元素)的典型示例。

列举JDK中可用的模块

Java 9的一个重要组织方面是将JDK划分为模块,以支持JEP 200中概述的各种配置。要列出模块,你可以在JDK的bin文件夹中使用java命令,带--list-modules选项:

java --list-modules
java --list-modules | grep -e 'java\\'.''
java --list-modules | grep "java\\."

JDK的模块集包括:

  • 实现Java SE规范的标准模块(名称以Java .*开头)
  • JavaFX模块(名称以JavaFX .*开头)
  • 特定于jdk的模块(名称以jdk.*开头)
  • oracle特定模块(名称以oracle.*开头)

每个模块名后面都有一个版本字符串。在本例中,我使用的是JDK 12.0.2版本,所以每个模块后面都跟着版本字符串@12.0.2。

比较JAVA 8 与JAVA 9 应用结构

Java SE 8 应用程序:

Java 9 应用程序:

在 Java 8 和更早的应用程序中,顶级组件是包。 它将一组相关类型放入一个组中。 它还包含一组资源。

Java 9 应用程序与 Java 8 没有太大区别。 它引入了一个新组件,模块,用于将一组相关的包放入一个组中。 并且还引入了另一个新组件:模块描述符module-info.java。 (这是个重要的东西)

Java 8 应用程序将包作为顶级组件,而 Java 9 应用程序将模块作为顶级组件。

顺便说一句,每个 Java 9 模块只能是一个具有一个模块描述符的模块。 与 Java 8 包不同,您不能将多个模块构建到一个模块中。

以下是 Java 9 模块中主要组件的一个很好的列表:

  • One module
  • Module name
  • Module descriptor
  • Set of packages
  • Set of types and resources

资源可以是模块描述符或任何其他属性或 XML。

模块和模块描述符

模块的基本规则

在开发任何 Java 9 模块时,您应该记住以下重要的基本规则:

  • 每个模块都有一个唯一的名称

    因为模块存在于 JVM 的全局空间中,所以每个模块都应该有一个唯一的名称。 与包和 JAR 文件名一样,您可以使用反向域名模式来定义模块名称。

  • 每个模块在源文件中都有一些描述

    模块描述在一个名为 module-info.java 的源文件中表示,并且应该完全像这样命名。 每个模块应该有一个且只有一个模块描述符(module-info.java)。模块描述符是一个 Java 文件。 它不是 XML、文本或属性文件。

  • 模块描述符文件放在顶层目录

    顶层目录是模块的根文件夹。

  • 每个模块可以有任意数量的包和类型

    一个模块可以依赖于任意数量的模块

模块描述符

在 Java 9 模块中,模块描述符是包含描述模块的模块元数据的资源。 它不是 XML 或属性文件; 它是一个普通的 Java 文件。

您必须将此文件命名为module-info.java并将其放在模块的根文件夹中。 与其他 Java 源文件一样,模块文件使用 javac 命令编译成 module-info.class

使用module关键字创建模块描述符:

module  
  // Module Meta Data goes here.

模块元数据

一个模块包含以下基本元数据:

  • 一个唯一的名字
  • exports 子句
  • requires 子句

元数据保存在模块描述符中。

module eg.com.taman.mod1 
   exports eg.com.taman.service;
   requires eg.com.taman.mod1;

模块描述符的要点

  • 模块描述符可以只包含模块名,其他什么都不包含;没有 exportsrequires 子句。
  • 模块描述符可以由一个或多个不带requires子句的exports子句组成;这意味着它将包导出到其他模块,但不依赖于任何其他模块——它是一个独立的模块。
  • 模块描述符可以同时具有exportsrequires子句;这意味着它将包导出到其他模块并使用其他模块的包——因为它依赖于其他模块,所以它不是一个独立的模块。
  • 模块描述符可以有0个、1个或多个require子句。

模块路径

类路径是用户定义和内置的一系列类和包或 JAR。JVM 或 Java 编译器需要类路径来编译应用程序或类。

在 Java 9 之前,编译器和运行时通过类路径定位类型:包含已编译 Java 类的文件夹和库归档文件列表,以及提供给 javacjava 命令的选项。因为可以从几个不同的位置加载类型,所以搜索这些位置的顺序会导致应用程序脆弱。

模块路径是一系列模块(以文件夹或 JAR 格式提供)。

模块和模块描述符提供的可靠配置有助于消除许多此类运行时类路径问题。每个模块都明确声明其依赖关系,这些依赖关系在应用程序启动时解决。

modulepath 只能包含每个模块中的一个,并且每个包只能在一个模块中定义。如果两个或多个模块具有相同的名称或导出相同的包,则运行时会在运行程序之前立即终止。

模块描述符的详解

上一节中对模块描述符的定义、位置、组成内容做了简要的说明。接下来详细的说明模块声明指令以及如何创建模块声明以指定模块的依赖项(使用 requires 指令)以及模块可用于其他模块的哪些包(使用 exports 指令)。

模块描述符是在名为module-info.java的文件中定义的模块声明的编译版本。每个模块声明都以关键字module开头,后跟一个唯一的模块名称和用大括号括起来的模块主体:

module moduleName 

模块声明的主体可以是空的,也可以包含各种模块指令,包括requiresexportsprovides...withusesopens(我将在本教程中讨论每一个)。你可以在这里看到其中的一些:

requires指令和模块依赖

requires 模块指令指定该模块依赖于另一个模块——这种关系称为模块依赖关系。

当模块 A 需要模块 B 时,模块 A 被称为读取模块 B,模块 B 被模块 A 读取。

要指定对另一个模块的依赖,您可以使用 requires,如下所示:

requires modulename;

还有一个 requires 静态指令来指示模块在编译时是必需的,但在运行时是可选的。

requires static <modulename>;

依赖传递

当前模块A的依赖B,在其他模块C依赖当前模块A时,会同时依赖当前模块自身的依赖B。可以在当前模块中使用 requires 传递指令:

要指定对另一个模块的依赖并确保读取您的模块的其他模块也读取该依赖(称为隐含可读性)

requires transitive <modulename>;

export指令与exports…to…指令

不管是export 指令还是exports…to…指令,所有导出模块指令都遵循以下的定义:

导出模块指令指定模块的包之一,其their nested public and protected types应该可供所有其他模块中的代码访问。

严格导出:模块my.module下的包my.package只给模块other.moduleanother.module使用,其他模块无法使用。

module my.module
  exports my.package to other.module, another.module;

需要此功能以避免将内部包暴露给所有模块,同时允许它们仅由选定的友好模块访问(或换句话说,强封装)。

例如,JDK java.base有许多不应该向所有人公开的包。 这是java.base模块的相关片段:

module java.base 
    .....
exports com.sun.security.ntlmtojava.security.sasl;
exports jdk.internal to jdk.jfr;
exports jdk.internal.jimage to jdk.jlink;
exports jdk.internal.jimage.decompressor to jdk.jlink;
exports jdk.internal.jmod to jdk.compiler, jdk.jlink;
exports jdk.internal.loader to java.instrument, java.logging;
exports jdk.internal.logger to java.logging;
exports jdk.internal.math to java.desktop;
    .....

高级模块声明

  • 服务指令:
    • 如何提供服务 (with provides...with)
    • 服务的消费方式 (with uses)
  • 反射指令: 源模块允许反射发生的其他模块(使用 open 修饰符和opens...to

服务指令

Java 长期以来通过 java.util.ServiceLoader 类支持服务,该类在运行时通过搜索类路径来定位服务提供者。 对于模块中定义的服务提供者,服务加载器必须考虑如何在可观察的模块集中定位这些模块,解决它们的依赖关系,并使提供者可用于使用相应服务的代码。

服务允许服务消费者模块和服务提供者模块之间的松散耦合。

假设这个 eg.com.taman.app 模块使用 mysql 数据库:

还假设在具有声明的可观察模块中提供了 MySQL JDBC 驱动程序:

module com.mysql.jdbc 
    requires java.sql;
    requires org.slf4j;
    
    exports com.mysql.jdbc;

  • org.slf4j 是驱动程序使用的日志库
  • com.mysql.jdbc 是包含 java.sql.Driver 服务接口实现的包

为了让java.sql模块使用这个驱动,ServiceLoader类必须能够通过反射实例化驱动类; 为此,模块系统必须将驱动模块添加到模块图中并解决其依赖关系。

为了完成这个任务,模块系统必须识别先前解析的模块对服务的任何使用,然后从可观察的模块集中定位和解析提供者。模块系统可以通过扫描模块工件中的类文件以查找ServiceLoader::load方法的调用来识别服务的使用,但这将是不可靠且缓慢的。

为了使它更简洁和更容易的任务,有user模块指令。 该指令指定此模块使用的服务,使模块成为服务使用者。 您可以在模块的声明中使用uses子句来表达这一点(之后版本的JDK中的模块描述文件内容已不是如此):

module java.sql 
    requires public java.logging;
    requires public java.xml;
    
    exports java.sql;
    exports javax.sql;
    exports javax.transaction.xa;
    
	uses java.sql.Driver;

与之配套的是provides...with module指令,它指定模块提供服务实现,使模块成为服务提供者。 服务是实现接口或扩展使用指令中指定的抽象类的类的对象。

换句话说,指令的provides部分指定模块的uses指令中列出的接口或抽象类,指令的with部分指定实现接口或扩展抽象类的类的名称。

对于一个模块来说,提供特定服务的实现也是基本的。 您可以在模块的声明中使用提供子句表达该功能:

module com.mysql.jdbc 
    requires java.sql;
    requires org.slf4j;
    
    exports com.mysql.jdbc;
    
    provides java.sql.Driver with com.mysql.jdbc.Driver;

通过阅读这些模块的声明,很容易看出其中一个使用了另一个提供的服务。

在模块声明中声明服务提供和服务使用关系具有提高效率和清晰度之外的优势。 可以在编译时解释服务声明,以确保服务的提供者和用户都可以访问服务接口。

服务提供者声明可以进一步确保提供者确实实现了他们声明的服务接口。 服务使用声明可以通过提前编译和链接工具来解释,以确保可观察的提供者在运行前被适当地编译和链接。

反射指令

在 Java 9 之前,您可以使用反射来了解包中包含的类型以及特定类型的所有成员,甚至是私有成员,无论您是否希望允许外部人员拥有这种能力,因此没有真正封装。

模块系统的一个关键规定是强封装; 因此,模块中的类型不能被其他模块访问,除非它是公共类型并且您导出它的包。 你只公开你想公开的包。

三种控制模式

  • Runtime-only access to a package 允许仅运行时访问包

    形式为 opens packagename 的 opens 模块指令,这表明特定包的公共类型(及其嵌套的公共和受保护类型)只能在运行时由其他模块中的代码访问。 此外,指定包中的所有类型(以及所有类型的成员)都可以通过反射访问。

    module eg.com.taman 
        opens eg.com.taman.lib;
    
    
  • Runtime-only access to a package by specific modules 允许特定模块仅运行时访问包

    一个 opens…to 形式的模块指令 opens package-to-comma-separated-list-of-modules,这表明特定包的公共类型(及其嵌套的公共和受保护类型)可以被列出的代码访问 仅在运行时的模块。 此外,指定包中的所有类型(以及所有类型的成员)都可以通过反射访问指定模块中的代码。

    module eg.com.taman 
        opens eg.com.taman.lib toeg.com.taman.util,eg.com.taman.math;
    
    
  • Runtime-only access to all packages in a module 允许仅运行时访问模块中的所有包

    open module modulename 形式的打开模块的模块名称,如果给定模块中的所有包都应该在运行时访问并通过反射到所有其他模块,则可以打开整个模块,如下所示:

    open module modulename 
    // module directives
    
    

    默认情况下,对包具有运行时反射访问权限的模块可以看到包的公共类型(及其嵌套的公共和受保护类型); 但是,其他模块中的代码可以访问公开包中的所有类型以及这些类型中的所有成员,包括私有成员。

    有关使用反射访问所有类型成员的更多信息,请访问 The Java Tutorials on Trail: The Reflection API.

限制的关键字

在 JPMS 中,您可以使用 exports、module、open、opens、provides、requires、to、transitive、uses 和 with 关键字来描述模块描述符中的模块元数据。 那么 Java 中的所有模块描述符指令都是新的受限关键字吗?

它们都不是关键字——它们只是模块声明中的关键字。 仅在模块描述符文件(如 module-info.java)中的关键字。


参考文章

A primer of module basics and rules: https://developer.ibm.com/tutorials/java-modularity-2

Modularity + encapsulation = security: https://developer.ibm.com/tutorials/java-modularity-3

以上是关于JDK模块化之模块的基础概念的主要内容,如果未能解决你的问题,请参考以下文章

py基础之模块与包

python之模块和包

Python基础之模块

JDK模块化之简单示例

JDK模块化之简单示例

JDK的模块化之Overview