Java模块系统介绍

上篇文章介绍了什么是模块化,以及Java模块化解决的问题。本文将介绍Java模块化的相关概念及具体写法。本文将从如下两个方面介绍模块化:

  1. 模块描述符
  2. 服务

1. 模块描述符

为了体现模块之间的关系,必须定义全新的模块描述文件,类似于Maven中的pom文件,Java9称之为模块描述符。

模块描述符是一个固定名称的java文件,所有的模块描述符文件名称固定位 module-info.java,其内部存在特定的结构。

下面是两个示例,其中easytext.cli模块依赖easytext.analysis模块。

module easytext.analysis {
   exports javamodularity.easytext.analysis;
}
module easytext.cli {
   requires easytext.analysis;
}

上面示例中一共有三个新增的关键字,这三个关键字也是模块化系统中最常用的关键字。

  1. module: 用来定义一个模块,后面紧跟模块名称,在同一个模块路径下,模块名称不允许相同。
  2. exports: 用来指定开放那个包作为API供外部调用,没有开放的api不允许被调用。
  3. requires: 用来执行依赖的模块,依赖必须显示指定。

通过exports和requires的配合使用,达到模块化的目的,即强封装、显式依赖。

为了描述这种依赖关系,Java9新增了一个概念,名为可读性(readability)easytext.cli模块依赖easytext.analysis模块,也可以说 easytext.cli模块可以读取easytext.analysis模块。

模块路径(module path) 和 类路径(class path)

类路径(class path)是指Java编译和运行的基础目录,类路径下所有文件都是平行的,不存在层级关系,一旦服务启动,所有类都可能会被加载。
模块路(module path)径是指存放Java模块的目录,并不是模块路径下的所有模块都会被加载,其以根模块(root module)为起点,使用模块描述符递归查找依赖的模块,它存在层级关系,是一个树状结构。

可读性(readability) 和 可访问性(accessible)

可访问性(accessible)是指public、protected、(default)、private这四个级别,它们作用在类与类之间,提供访问控制。
可读性(readability)则提供模块与模块之间的访问控制,是对可访问性的功能补充。
可读性(readability)不止作用于运行阶段,与可访问性一样也作用于编译阶段,即未明确指定依赖的模块,编译将直接报错。

隐式可读性

有时候简单的依赖关系不能满足一些特定场景,比如依赖传递特性。模块化系统通过transitive关系字,解决依赖的传递问题,这也称为隐式可读性

如下所示,java.se模块中隐式依赖了一些模块,当其它模块依赖java.se时,会自动依赖这些模块。通过隐式可读性,我们可以聚合不同的模块,为模块分组,java.se就是一个最常用的模块组合。

module java.se {
  requires transitive java.desktop;
  requires transitive java.sql;
  requires transitive java.xml;
  // 省略
}

限制导出

在某些情况下,可能只需要将包暴露给特定的某些模块。此时,可以在模块描述符中使用限制导出。可以在java.xml模块中找到限制导出的示例:

module java.xml{
  ...
  exports com.sun.xml.internal.stream.writers to java.xml.ws
  ...
}

一般来说,应该避免在模块之间使用限制导出。使用限制导出意味着在导出模块和允许的使用者之间建立了直接的联系。该特性最大的目的是为了处理历史问题,如对旧版本JDK模块化。

2. 服务(Service)

应用程序模块化后,被exports的包,通常仅包含接口,不包含具体实现,且实现类的个数可能是一个或多个。

为了充分解耦,接口的实现类不能由客户端创建,仅能由服务端提供,即Analyzer analyzer = ???。对于这种情况,工厂模式是一个方案。

public class AnalyzerFactory {

   public static List<String> getSupportedAnalyses() {
     return List.of(FleschKincaid.NAME, Coleman.NAME);
   }

   public static Analyzer getAnalyzer(String name) {
      switch (name) {
         case FleschKincaid.NAME: return new FleschKincaid();
         case Coleman.NAME: return new Coleman();
         default: throw new IllegalArgumentException("No such analyzer!");
      }
   }
}

通过引入工厂模式,客户端只需获得服务名称列表后,根据名称选择使用哪个服务即可,而无需知道具体的实现类。

当我们想为Analyzer增加一个实现类时,必须要修改AnalyzerFactory才能实现,这违背了开闭原则。同时AnalyzerFactory自身也必须依赖所有的实现,这样exports的包中也包含了实现类。

为了解决这个问题,模块化系统提供了服务(Service)的功能。通过使用ServiceLoader API可在模块描述符和代码中表示服务。

服务提供者的模块描述符如下,通过provides with关键字,确定了接口的实现类。可以通过ServiceLoader.load的方式获得所有的Analyzer实现。

module easytext.analysis.coleman {
  requires easytext.analysis.api;
  provides javamodularity.easytext.analysis.api.Analyzer
      with
        javamodularity.easytext.analysis.coleman.ColemanAnalyzer;
}
public interface Analyzer {

   String getName();

   double analyze(List<List<String>> text);

   static Iterable<Analyzer> getAnalyzers() {
     return ServiceLoader.load(Analyzer.class);
   }

}

客户端的模块描述符如下,通过uses关键字,指定想要使用的接口。

module easytext.cli {
  requires easytext.analysis.api;
  uses javamodularity.easytext.analysis.api.Analyzer;
}
Iterable<Analyzer> analyzers = Analyzer.getAnalyzers();

如果存在多个提供者,但只对“最好的”实现感兴趣,该怎么做呢?Java模块系统不可能知道哪个实现最合适,所以只能由应用程序自己决定。

服务绑定的模块解析

服务的provides with为解析过程添加了另一个维度。模块路径中使用provides with关键字的模块,将被自动加载,而无需再使用requires显示声明。

服务(Service)是可选的

requiresexports不同,服务(Service)是可选的,只有在需要的时候采用即可。

3. 小结

本文介绍了Java模块化的基本使用方式,过程中涉及的关键字汇总如下。

关键字 描述
module 声明模块名称,在模块路径下,模块名称必须是唯一的。
requires 声明依赖的模块,只有模块被依赖后才能正常使用。
transitive 跟随requires使用,时依赖可以传递。
exports 声明当前模块开放的package,只有被开放的package才能正常使用。
uses 声明要使用的服务,该关键字没有requires的功能,必须单独声明requires指定uses接口的所在的模块。
provides with 声明服务的提供者,模块解析过程中,将自动解析服务提供者所在的模块,类似服务提供者被requires了。

参考的文章

Java 9模块化开发:核心原则与实践 (O’Reilly精品图书系列)
java9-modularity/examples


文章作者: 沉迷思考的鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沉迷思考的鱼 !
评论
 上一篇
Java模块化开发通用设计指南 Java模块化开发通用设计指南
模块化不仅仅是一个实现问题,也是一个设计和架构的问题。通过模块化,可以应对需求、环境、团队以及其他不可预见事件所带来的变化。 本章将讨论模块化开发通用设计指南,以提高使用模块所构建系统的可维护性、灵活性和可重用性,这些模式和设计实践中的大部
2019-10-28
下一篇 
Java 9 新特性概述 Java 9 新特性概述
Java9正式发布于2017年9月21日,作为Java8之后3年半才发布的新版本,Java9带来了很多重大的变化,其中最重要的改动是Java模块化的引入。 本文对Java9中包含的新特性做了概括性的介绍,可以帮助你快速了解Java9。 1
2019-09-14
  目录