在Java 11中创建一个简单的模块化应用教程


模块化编程使人们能够将代码组织成独立的,有凝聚力的模块,这些模块可以组合在一起以实现所需的功能。
本文摘自Nick Samoylov和Mohamed Sanaulla撰写的一本名为Java 11 Cookbook - Second Edition的书。在本书中,您将学习如何使用Java 11中的类和接口实现面向对象的设计 。
可以在GitHub上找到本教程中显示的示例的完整代码。

您应该想知道这种模块化是什么,以及如何使用Java创建模块化应用程序 。在本文中,我们将通过一个简单的示例来尝试清除在Java中创建模块化应用程序的困惑 。我们的目标是向您展示如何创建模块化应用程序; 因此,我们选择了一个简单的例子,以便专注于我们的目标。

做什么
我们的示例是一个简单的高级计算器,它检查数字是否为素数,计算素数之和,检查数字是否为偶数,并计算偶数和奇数之和。

做好准备
我们将应用程序分为两个模块:

  • math.util模块包含用于执行数学计算的API
  • calculator模块启动了一个高级计算器

怎么做

1. 让我们实现com.packt.math.MathUtil中的API,从isPrime(Integer number)API开始:

public static Boolean isPrime(Integer number){
          if ( number == 1 ) { return false; }
          return IntStream.range(2,num).noneMatch(i -> num % i == 0 );
        }

2. 实现sumOfFirstNPrimes(Integer count)
   

public static Integer sumOfFirstNPrimes(Integer count){
          return IntStream.iterate(1,i -> i+1)
                          .filter(j -> isPrime(j))
                          .limit(count).sum();
        }

3. 让我们写一个函数来检查数字是否是偶数:

 

public static Boolean isEven(Integer number){
          return number % 2 == 0;
        }


4. 非isEven结果告诉我们这个数字是否是奇数。我们可以使用函数来查找前N个偶数和前N个奇数之和,如下所示:

  public static Integer sumOfFirstNEvens(Integer count){
          return IntStream.iterate(1,i -> i+1)
                          .filter(j -> isEven(j))
                          .limit(count).sum();
        }
public static Integer sumOfFirstNOdds(Integer count){
return IntStream.iterate(1,i -> i+1) .filter(j -> !isEven(j)) .limit(count).sum();
}

我们可以在前面的API中看到重复以下操作:
  • 从数字1开始的无限数字序列 
  • 根据某些条件过滤数字
  • 将流的数量限制为给定的计数
  • 找到由此获得的数字之和

根据我们的观察,我们可以重构前面的API并将这些操作提取到一个方法中,如下所示:

Integer computeFirstNSum(Integer count,
                                 IntPredicate filter){ 
  return IntStream.iterate(1,i  - > i + 1)
                  .filter(filter)
                  .limit(count).sum(); 
 }

这里  count是我们需要找到的总和的数量限制,并且  filter是选择求和数的条件。
让我们根据刚刚进行的重构重写API:

public static Integer sumOfFirstNPrimes(Integer count){ return computeFirstNSum(count, (i -> isPrime(i))); }
public static Integer sumOfFirstNEvens(Integer count){ return computeFirstNSum(count, (i -> isEven(i))); } public static Integer sumOfFirstNOdds(Integer count){ return computeFirstNSum(count, (i -> !isEven(i)));

到目前为止,我们已经看到了一些围绕数学计算的API。

开始正题
让我们将这个小实用程序类作为名为的模块的一部分  math.util。以下是我们用于创建模块的一些约定:

  1. 将与模块相关的所有代码放在一个名为的目录下math.util,并将其视为我们的模块根目录。
  2. 在根文件夹中,插入名为module-info.java.的文件  
  3. 将包和代码文件放在根目录下。

module-info.java包含什么?
  • 模块的名称
  • 它导出的包,即可供其他模块使用的包
  • 它依赖的模块
  • 它使用的服务
  • 它为其提供实施的服务

我们的math.util模块不依赖于任何其他模块(当然,java.base模块除外)。但是,它使其API可用于其他模块(如果没有,那么这个模块的存在是有问题的)。让我们继续把这个陈述放到代码中:

module math.util { 
  exports com.packt.math; 
}

我们告诉Java编译器和运行时我们的math.util 模块正在将com.packt.math包中的代码导出到任何依赖的模块math.util。
可以在以下位置找到此模块的代码  Chapter03/2_simple-modular-math-util/math.util。
现在,让我们创建另一个使用该math.util模块的模块计算器。该模块有一个Calculator类,其工作是接受用户选择执行哪个数学运算,然后执行操作所需的输入。用户可以从五种可用的数学运算中进行选择:
  • 素数检查
  • 偶数号检查
  • N素数总和
  • N偶数总和
  • N奇数总和

我们在代码中看到这个:

private static Integer acceptChoice(Scanner reader){
  System.out.println("************Advanced Calculator************");
  System.out.println(
"1. Prime Number check");
  System.out.println(
"2. Even Number check");
  System.out.println(
"3. Sum of N Primes");
  System.out.println(
"4. Sum of N Evens");
  System.out.println(
"5. Sum of N Odds");
  System.out.println(
"6. Exit");
  System.out.println(
"Enter the number to choose operation");
  return reader.nextInt();
}

然后,对于每个选项,我们接受所需的输入并调用相应的MathUtilAPI,如下所示:

switch(choice){
  case 1:
    System.out.println("Enter the number");
    Integer number = reader.nextInt();
    if (MathUtil.isPrime(number)){
      System.out.println(
"The number " + number +" is prime");
    }else{
      System.out.println(
"The number " + number +" is not prime");
    }
  break;
  case 2:
    System.out.println(
"Enter the number");
    Integer number = reader.nextInt();
    if (MathUtil.isEven(number)){
      System.out.println(
"The number " + number +" is even");
    }
  break;
  case 3:
    System.out.println(
"How many primes?");
    Integer count = reader.nextInt();
    System.out.println(String.format(
"Sum of %d primes is %d"
          count, MathUtil.sumOfFirstNPrimes(count)));
  break;
  case 4:
    System.out.println(
"How many evens?");
    Integer count = reader.nextInt();
    System.out.println(String.format(
"Sum of %d evens is %d"
          count, MathUtil.sumOfFirstNEvens(count)));
  break;
  case 5: 
    System.out.println(
"How many odds?");
    Integer count = reader.nextInt();
    System.out.println(String.format(
"Sum of %d odds is %d"
          count, MathUtil.sumOfFirstNOdds(count)));
  break;
}

让我们calculator以与为模块创建模块相同的方式为模块创建模块定义math.util:

module calculator{
  requires math.util;
}

在前面的模块定义中,我们提到  calculator模块依赖于  math.util模块使用  required 关键字。
让我们编译代码:

javac -d mods --module-source-path . $(find . -name "*.java")

--module-source-path 命令是  javac新的命令行选项,用于指定模块源代码的位置。

让我们执行前面的代码:

java --module-path mods -m calculator/com.packt.calculator.Calculator

--module-path 命令类似于--classpath,是新java的命令行选项   ,指定已编译模块的位置。
运行上述命令后,您将看到计算器正在运行。

我们提供了脚本来测试Windows和Linux平台上的代码 。请使用run.bat用于Windows和run.sh用于 Linux的

原理
现在您已经完成了示例,我们将了解如何对其进行概括,以便我们可以在所有模块中应用相同的模式。我们遵循特定的约定来创建模块:

| application_root_directory 
| --module1_root 
| ---- module-info.java 
| ---- com 
| ------ packt 
| -------- sample 
| --------- -MyClass.java 
| --module2_root 
| ---- module-info.java 
| ---- com 
| ------ packt 
| -------- test 
| ------- ---MyAnotherClass.java

我们将特定于模块的代码放在其文件夹中,并在文件夹module-info.java 的根目录下放置相应的文件。这样,代码组织得很好。
让我们看一下module-info.java可以包含什么。根据Java语言规范(http://cr.openjdk.java.net/~mr/jigsaw/spec/lang-vm.html),模块声明具有以下形式:

{Annotation} [open] module ModuleName {{ModuleStatement}}


这是语法,解释如下:

  • {Annotation}:这是表单的任何注释@Annotation(2)。
  • open:此关键字是可选的。开放模块通过反射在运行时访问其所有组件。但是,在编译时和运行时,只能访问显式导出的那些组件。
  • module:这是用于声明模块的关键字。
  • ModuleName:这是模块的名称,该模块是有效的Java标识符,.在标识符名称之间允许使用dot() - 类似于  math.util。
  • {ModuleStatement}:这是模块定义中允许的语句的集合。让我们接下来展开。

模块语句具有以下形式:

ModuleStatement:
  requires {RequiresModifier} ModuleName ;
  exports PackageName [to ModuleName {, ModuleName}] ;
  opens PackageName [to ModuleName {, ModuleName}] ;
  uses TypeName ;
  provides TypeName with TypeName {, TypeName} ;

模块语句在这里被解码:

  • requires:这用于声明对模块的依赖。{RequiresModifier}可以是传递的,静态的,或两者兼而有之。传递意味着依赖于给定模块的任何模块也隐式地依赖于给定模块传递所需的模块。静态意味着模块依赖在编译时是必需的,但在运行时是可选的。一些例子是  requires math.util,requires transitive math.util和  requires static math.util。
  • exports:这用于使依赖模块可以访问给定的包。或者,我们可以通过指定模块名称来强制包对特定模块的可访问性,例如  exports com.package.math to claculator。
  • opens:这用于打开特定包。我们之前看到,我们可以通过open使用模块声明指定关键字来打开模块。但这可能是限制性较小的。因此,为了使其更具限制性,我们可以使用openskeyword- 在运行时打开一个特定的反射访问包opens com.packt.math。
  • uses:这用于声明可通过可访问的服务接口的依赖项java.util.ServiceLoader。服务接口可以位于当前模块中,也可以位于当前模块所依赖的任何模块中。
  • provides:这用于声明服务接口并为其提供至少一个实现。可以在当前模块或任何其他相关模块中声明服务接口。但是,必须在同一模块中提供服务实现; 否则,将发生编译时错误。

我们将在使用服务中更详细地查看uses和provides子句,  以在消费者和提供者模块  配方之间创建松散耦合。
可以使用--module-source-path命令行选项一次编译所有模块的模块源。这样,所有模块都将被编译并放置在该-d选项提供的目录下的相应目录中。例如,  javac -d mods --module-source-path . $(find . -name "*.java") 将当前目录中的代码编译到mods 目录中。
运行代码同样简单。我们使用命令行选项指定编译所有模块的路径  --module-path。然后,我们使用命令行选项提及模块名称以及完全限定的主类名称  -m,例如  java --module-path mods -m calculator/com.packt.calculator.Calculator。