在Java 11中建立一個簡單的模組化應用教程

banq發表於2019-02-21

模組化程式設計使人們能夠將程式碼組織成獨立的,有凝聚力的模組,這些模組可以組合在一起以實現所需的功能。
本文摘自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。
 

相關文章