2021年12月25日 星期六

5.4 在命令列(command line)使用模組指令選項-jmod

JAR檔案在Java 9之前就已經存在,主要用來打包編譯好的類別檔;在Java 9之後也提升為可支持模組化JAR。除了使用已經存在的JAR檔之外,Java 9為封裝模組又引入了兩種新檔案格式,分別是「JMOD」和「JIMAGE」。

這兩種格式的介紹不在本書範圍,目前只需要知道:

1. Oracle建議大多數開發模組的任務依然使用JAR檔案,只有在少數情形才使用JMOD檔案:

2. 指令jmod僅用於處理JMOD檔案,以下簡列一些jmod指令的選項:

選項

功能

create

新建JMOD檔案。

extract

JMOD檔案中提取檔案,類似解壓縮。

describe

描述模組內容。

list

列出JMOD檔案中的檔案清單。

hash

JMOD檔案的雜湊字串。


4.0 認識module-info.java檔案的宣告關鍵字

 由先前範例已經成功建立了基本的模組,接下來要了解更多關於module-info.java檔案編寫的宣告指令,如exports、requires、provides、uses和opens等出現和使用的時機。

模組宣告指令exports和requires是Java關鍵字嗎?

先前我們提供過Java的關鍵字列表,但exports和requires並未出現在列表,甚至也沒有module這個字。事實上,這些字屬於編寫模組資訊檔module-info.java內的關鍵字,一旦不在這範圍就不是,因此在撰寫類別或介面時依然可以使用這些字作為變數名稱。

Java為了考慮向前相容(如使用Java 8編寫的程式碼,依然可以通過Java 11的編譯),不能隨意增加關鍵字,否則將導致升版之後過去程式必須改寫。因為模組化是Java 9才出現的功能,把這些關鍵字定義在module-info.java範圍內就沒有問題。



5.3 在命令列(command line)使用模組指令選項-jdeps

指令jdeps提供有關模組內依賴項目的資訊。

相比於指令javajar使用選項「--describe-module」,該指令除了檢視模組資訊檔之外,它還查看程式碼,因此可以反應更詳實的結果。

先從一個簡單的範例開始,使用選項「-summary」提供模組zoo.animal.feedingJAR檔的依賴項目的概略說明:

jdeps 
-summary 
mods/zoo.animal.feeding.jar

選項「-summary」可以使用「-s」予以簡化。兩者會有一樣結果:

輸出顯示只有一個套件並依賴於內建的java.base模組:

【結果】

zoo.animal.feeding -> java.base

若未使用-summary選項則可以得到完整的結果。指令為:

jdeps 
mods/zoo.animal.feeding.jar
結果為:

文字結果:

1

zoo.animal.feeding

2

 [file:///C:/java11/code/zoo.staff/mods/zoo.animal.feeding.jar]

3

   requires mandated java.base (@11.0.12)

4

zoo.animal.feeding -> java.base

5

   zoo.animal.feeding      -> java.io        java.base

6

   zoo.animal.feeding      -> java.lang      java.base

【說明】

1

模組名稱。

2

模組檔案路徑。

3

模組相依項目與版本。

4

與使用-summary選項結果相同。

5

模組zoo.animal.feeding使用套件java.io,屬於模組java.base

類別Task使用System.out,稱為「標準輸出」,涉及Java I/O

6

模組zoo.animal.feeding使用套件java.lang,屬於模組java.base

類別Task使用System類別,屬於套件java.lang

接下來檢視一個具備更複雜的模組依賴關係的zoo.animal.careJAR檔案。因為zoo.animal.care依賴於zoo.animal.feeding,分析時必須使用選項「--module-path」告知相依模組的路徑。先前檢視zoo.animal.feeding不需要該選項是因為所有依賴模組都內建在JDK中。

指令為:

jdeps 
-summary
--module-path mods
mods/zoo.animal.care.jar

注意這裡的「--module-path」選項不能以-m-p取代。

結果為:

文字結果:

zoo.animal.care -> java.base

zoo.animal.care -> zoo.animal.feeding

可以看出zoo.animal.care模組依賴自定義的zoo.animal.feeding模組與Java內建的java.base模組。

去除選項-summary改以完整模式下執行:

jdeps --module-path mods mods/zoo.animal.care.jar

【結果】

1

zoo.animal.care

2

 [file:///C:/java11/code/zoo.staff/mods/zoo.animal.care.jar]

3

   requires mandated java.base (@11.0.12)

4

   requires transitive zoo.animal.feeding

5

zoo.animal.care -> java.base

6

zoo.animal.care -> zoo.animal.feeding

7

   zoo.animal.care.details     -> java.lang            java.base

8

   zoo.animal.care.details     -> zoo.animal.feeding    zoo.animal.feeding

9

   zoo.animal.care.medical    -> java.lang            java.base

可以看出行56與使用-summary選項結果相同,行7-9則輸出相依的套件與模組細節。

5.2 在命令列(command line)使用模組指令選項-jar

java指令一樣,指令jar也具備選項可以描述一個模組,如下:

jar 
--file mods/zoo.animal.feeding.jar 
--describe-module

【說明】

2

使用選項--file指定JAR檔案位置,可以使用-f取代。

3

使用選項--describe-module描述模組JAR檔案內容,可以使用-d取代。

輸出結果和使用java指令描述模組略有不同,主要差別在行1尾端:

【結果】

1

zoo.animal.feeding jar:file:///C:/java11/code/zoo.staff/mods/zoo.animal.feeding.jar/!module-info.class

2

exports zoo.animal.feeding

3

requires java.base mandated

不過並沒有甚麼特殊意涵。只要了解選項「--describe-module」可以同時用於指令javajar即可。


5.1 在命令列(command line)使用模組指令選項-java

指令java除了可以執行Java SE類別裡的main()方法之外,還有與模組相關的選項,常見有以下3個:

java指令選項

作用

--describe-module

描述模組內容

--list-modules

列舉可用模組清單

--show-module-resolution

解析模組執行時的步驟

因為專案zoo.staffmods目錄內有打包好的全部的模組JAR檔案,因此執行指令時預設的路徑是專案zoo.staff的根目錄,以本書為例是C:\java11\code\zoo.staff

後續將逐一說明與示範。

使用選項--describe-module

假設我們拿到一個模組zoo.animal.feeding的JAR檔案,並且想了解它的模組結構。我們可以把該JAR檔案與以解壓縮並瀏覽module-info.java檔案如下。檔案內容顯示該模組exports一個套件並且不需要任何模組:

module zoo.animal.feeding {
    exports zoo.animal.feeding;
}
不過還有一種更簡單的方法,就是使用java指令的「--describe-module」選項來描述一個模組:
java -p mods
--describe-module zoo.animal.feeding
指令選項「--describe-module」可以使用「-d」簡化,所以執行指令「java -p mods -d zoo.animal.feeding」時可以得到相同的結果:

摘錄文字結果:

1

zoo.animal.feeding file:///C:/java11/code/zoo.animal.feeding/mods/zoo.animal.feeding.jar

2

exports zoo.animal.feeding

3

requires java.base mandated

說明:

1

輸出模組名稱與JAR實體檔案路徑。

2

exports套件zoo.animal.feeding,這部分與模組資訊檔內容相同。

3

這一行是模組系統自動加上的。

如同編寫類別程式碼時會自動imports基礎java.lang套件,模組系統也會自動註記所有模組都需要基本的java.base模組,關鍵字「mandated」就是如此意含,指出如java.base模組並沒有明確宣告在模組資訊檔中,但因為規格授權還是會自動出現。

類似的情境,針對模組zoo.animal.care,再比較模組資訊檔與java指令描述模組的差異。模組資訊檔如下:

module zoo.animal.care {
    exports zoo.animal.care.medical;
    requires transitive zoo.animal.feeding;
}

執行指令「java -p mods -d zoo.animal.care」將得到以下結果:

1

zoo.animal.care file:///C:/java11/code/zoo.animal.care/mods/zoo.animal.care.jar

2

exports zoo.animal.care.medical

3

requires java.base mandated

4

requires zoo.animal.feeding transitive

5

contains zoo.animal.care.details

指令結果比模組資訊檔多了行3與行5。行3在前例已經解釋,行5則是點出了模組內未以exports公開的套件。

模組中未使用exports公開的套件,將會使用contains宣告以表示供模組內部使用。

使用選項--list-modules

除了描述模組之外,還可以使用java指令列出可用的模組。未指定模組JAR檔案路徑時,將列出屬於JDK的模組:以下是指令
java --list-modules

的執行結果:

輸出的行數很多,這裡只節錄開頭幾行。內容是Java內建的所有模組及其版本號的列表,可以看出執行指令是Java11.0.12版。

若指令中包含zoo專案的所有模組JAR檔案:

java -p mods --list-modules

則執行結果就是前述的行內容加上4zoo專案的模組JAR檔:

使用選項--show-module-resolution

使用選項--show-module-resolution可以視為debug模組的一種手段。因為它會執行模組,並輸出過程,最後並輸出執行結果。
執行以下指令前先切換至專案zoo.animal.feeding的根目錄,如C:\java11\code\zoo.animal.feeding:
java --show-module-resolution 
-p src 
-m zoo.animal.feeding/zoo.animal.feeding.Task

以下節錄過程的一些輸出內容,最後一行是執行結果:

root zoo.animal.feeding file:///C:/java11/code/zoo.animal.feeding/src/

java.base binds jdk.localedata jrt:/jdk.localedata

java.base binds jdk.zipfs jrt:/jdk.zipfs

java.base binds jdk.charsets jrt:/jdk.charsets

java.base binds jdk.security.auth jrt:/jdk.security.auth

...

java.security.sasl requires java.logging jrt:/java.logging

java.naming requires java.security.sasl jrt:/java.security.sasl

jdk.security.jgss requires java.logging jrt:/java.logging

...

All are fed!

它首先列出根(root)模組,本例是zoo.animal.feeding;然後列出了java.base模組所包含的多行套件,也會列出具有相依關係的模組。最後它輸出指定類別zoo.animal.feeding.Task的執行結果。


4.3. 使用provides、uses、opens

宣告指令uses用於指示該模組相依於一個「服務(service)」,通常是介面(interface),如:

module service.consumer {
    uses some.serviceApi;
}

宣告指令provides用於指示該模組提供一個服務的實作(implementation),如:

module service.provider {
    provides some.serviceApi with some.serviceApiImpl;
}

最後一個宣告指令opens則和Java的映射(reflection)技術有關。

當使用多型時,Java的程式呼叫端在編譯(compile)時期可以知道物件參考的型別,但只有在執行(runtime)時期才能知道實際的實作。使用映射技術時,程式呼叫端在編譯時期甚至不需要知道物件參考的型別,但在執行時期就可以執行指定的物件方法。這並非初學者會接觸的範圍,但以現今大家對資訊安全的重視,這明顯具備一定程度的危險性!

有鑑於此,模組系統要求程式的呼叫端和被呼叫端都可以明確允許映射技術的使用!

以範例專案lab.reflection.provider為例,作為映射技術的被呼叫端,具備套件lab.reflection.provider.api和類別HelloWorld

package lab.reflection.provider.api;
public class HelloWorld {
    public String getGreeting() {
        return "hi, greeting from lab.reflection.provider.api";
    }
}

及模組資訊檔:

module lab.reflection.provider {
    exports lab.reflection.provider.api;
}

接下來建立專案lab.reflection.consumer作為映射技術的呼叫端。這個範例我們不使用javac的指令進行編譯,因此要先比照先前內容設定2Eclipse專案在模組部分的相依關係。

接下來先建立專案的模組資訊檔,宣告專案依賴模組lab.reflection.provider

module lab.reflection.consumer {
    requires lab.reflection.provider;
}
接下來建立套件lab.reflection.consumer.user與類別AccessByNormal。因為Eclipse已經完成專案Modulepath的設定,而且2個專案的模組資訊檔也有相應的exports和requires宣告,因此可以在行2直接import另一個專案的模組的套件與類別。
另外本類別示範類別的一般存取方式:
  1. imports使用類別,如行2。
  2. 建立物件與物件參考,如行6。
  3. 呼叫物件參考的方法,如行7。
下一個範例將使用映射技術,可以比對兩者的差異:
package lab.reflection.consumer.user;
import lab.reflection.provider.api.HelloWorld;
public class AccessByNormal {
    public static void main(String args[]) {
        try {
            HelloWorld om = new HelloWorld();
            System.out.println(om.getGreeting());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

接下來建立映射技術的呼叫者類別AccessByReflection,行2匯入的套件java.lang.reflect和類別Method用於映射技術:

package lab.reflection.consumer.user;
import java.lang.reflect.Method;
public class AccessByReflection {
  public static void main(String args[]) {
    try {
      Class c = Class.forName("lab.reflection.provider.api.HelloWorld");
      Method m = c.getMethod("getGreeting");
      System.out.println(m.invoke(c.getDeclaredConstructor().newInstance()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

整個範例中未曾建立類別HelloWorld的物件並呼叫方法getGreeting(),唯一相關的就是將類別名稱和方法名稱以「字串」表示,因此只要更換字串內容就可以呼叫不同類別的方法,這也是映射技術神奇和危險的地方!

請注意,不管是類別AccessByNormalAccessByReflection,在類別HelloWorld 所在的模組使用「exports」都是可以通過編譯且執行結果相同;這表示依賴關係的提供端模組使用exports關鍵字時,使用端模組不管在編譯(compile)和執行(runtime)時期都可以存取。

接下來把提供端模組的模組資訊檔由exports宣告改為「opens」:

module lab.reflection.consumer {
    opens lab.reflection.provider;
}
此時可以發現使用端模組的類別AccessByNormal因為無法存取lab.reflection.provider.api.HelloWorld而編譯失敗,但AccessByReflection依然可以正常執行:
這差異顯示了使用宣告指令opens只開放執行時期使用,exports則開放編譯和執行時期使用。這讓Java程式設計師對於釋出的模組函式庫的存取控制有更大的運用。