JDK 新特性學習筆記之模組系統

北冥有隻魚發表於2022-12-18

有兩條小魚快樂地遊著,碰到一條老魚從對面游過來。老魚向他們點頭問好:「早上好啊小夥子們,今天的水怎麼樣?」兩條小魚接著遊了一會兒,突然停了下來,一臉懵逼地看著對方:水是個什麼東西?

習以為常的就是水

模組系統是JDK 9的特性,後面的JavaFX學習筆記都會基於JDK 11,甚至更高版本。同時這個特性也是我比較感興趣的,進一步強化了Java的封裝能力。

回顧Java的特性

我想起剛畢業找工作時背的面試題,物件導向的三大特性是什麼? 想到這個問題我一瞬間居然沒想到答案,愣了一下才想起答案:

  • 封裝
  • 繼承
  • 多型

那什麼是封裝呢? 我的答案是隱藏複雜實現過程對外提供功能,像智慧手機一樣,我們直接操作螢幕就能實現聽音樂,看影片,打電話等操作。我覺得我的概括還是相當精準的,但我在搜尋引擎上搜尋了一下發現,我弄混了封裝和資訊隱藏,所謂封裝是將資料和運算元據的方法都放在類裡面,像膠囊一樣:
膠囊

封裝是指將資料與操作該資料的方法捆綁在一起。而資訊隱藏是隱藏實現細節。封裝和資訊隱藏常常出現在一起,以致於它們幾乎成了同義詞,在一些上下文,它們也的確是同義詞。封裝提供了邊界,而資訊隱藏則遮蔽複雜實現,這兩個常常出現在一起,我們在封裝的同時使用資訊隱藏。

那繼承呢? 繼承源於共性,不同的物件之間具備共性,那我們建模的時候就可以將共性抽出,將其當作父類,從而減少程式碼冗餘,增強程式碼的簡潔性。那多型呢?在《The Java™ Tutorials》(這個Java官方出的教程)在介紹多型的時候是這麼介紹的:

The dictionary definition of polymorphism refers to a principle in biology in which an organism or species can have many different forms or stages. This principle can also be applied to object-oriented programming and languages like the Java language. Subclasses of a class can define their own unique behaviors and yet share some of the same functionality of the parent class.

多型性的字典定義是指生物學中的一個原則,其中一個生物體或物種可以有許多不同的形式或階段。這一原則也適用於物件導向程式設計和Java語言等語言。類的子類可以定義自己獨特的行為,但也可以共享父類的一些相同功能。

多型進一步細分,功能(函式)過載與物件過載,函式過載就意味著函式在擁有不同的型別的引數或者不同數量引數的時候擁有相同的方法名。而物件則與繼承有關,一個父類可以有多個子類,子類可以複用父類的行為,當然也可以進行重寫,藉助多型可繼承我們重用已有的程式碼。

目前的問題

在《讓我們來聊聊前端的工程化》我們已經討論過了軟體危機,這裡再回憶一下:

1970年代和1980年代的軟體危機。在那個時代,許多軟體最後都得到了一個悲慘的結局,軟體專案開發時間大大超出了規劃的時間表。一些專案導致了財產的流失,甚至某些軟體導致了人員傷亡。同時軟體開發人員也發現軟體開發的難度越來越大。在軟體工程界被大量引用的案例是Therac-25的意外:在1985年六月到1987年一月之間,六個已知的醫療事故來自於Therac-25錯誤地超過劑量,導致患者死亡或嚴重輻射灼傷。

鑑於軟體開發時所遭遇困境,北大西洋公約組織在1968年舉辦了首次軟體工程學術會議,並於會中提出"軟體工程"來界定軟體開發所需相關知識,並建議"軟體開發應該是類似工程的活動"。軟體工程自1968年正式提出至今,這段時間累積了大量的研究成功案例,廣泛地進行大量的技術實踐,藉由學術界和產業界的共同努力,軟體工程正逐漸發展成為一門專業學科。

關於軟體工程的定義,在GB/T11457-2006《訊息技術 軟體工程術語》中將其定義為"應用電腦科學理論和技術以及工程管理原則和方法,按預算和進度,實現滿足使用者要求的軟體產品的定義、開發、和維護的工程或進行研究的學科"。

Therac-25: 是加拿大原子能有限公司(AECL) 在 Therac-6 和 Therac-20 裝置之後於 1982 年生產的一種計算機控制的放射治療機。它有時會給患者帶來比正常情況高數百倍的輻射劑量,導致死亡或重傷

那為了解決軟體危機中的軟體開發速度、軟體開發越來越複雜,構建軟體的程式語言引入了物件導向,著眼於強化於程式碼的重用性、可維護性。Java中我們如何複用別人的程式碼呢? 如果在一個專案裡面,我們通常會直接用,即在需要用到的類和方法裡面寫類名即可,IDE會幫我們自動引入。如果是第三方提供我們是透過jar這樣的形式來引入,JDK為我們提供的類庫,像HashMap、ArrrayList,這些都放在JDK中的一個jar中。任何一個Java檔案總是有一個public class,如果我構建了一個類庫,只對外提供一些類和介面該怎麼做呢,在JDK 8之前包含JDK8是做不到的,原因在於反射,私有的方法我照樣可以透過反射來訪問。這為後期的維護帶來了很大的麻煩,例如我原先的實現不夠好,對外提供的類和介面我不做改動,但我想改動不對外提供類的實現,但改動了不確定有沒有人用,別人在升級的時候可能就會問候我兩句。再有JDK的jar也太過臃腫,太過龐大了, 只有相當粗略的劃分, JDK 8大致提供的jar如下:

  • rt.jar 、charset.jar 、ckdrdara.jar、dnsns.jar、jaccess.jar、jce.jar、jfr.jar
  • jsse.jar、localedata.jar、management.jar、access-brige-64.jar、sunec.jar
  • sunjce.jar、sunjce_provide.jar、sunmscapi.jar、sunpkcs11.jar、zipfs.jar、nashron.jar

這些jar我們好像都不認識,其實rt.jar是我們的老熟人,集合類、併發集合都在裡面,所以rt.jar在JDK8有60m大小,Nashron是一個JavaScript引擎。所以如果我們開發服務端應用,我們不需要JavaScript引擎,在安裝JDK8的時候這個也會被安裝進去,包括AWT、SWING,儘管我們用不到,那我們自然提出這樣一個問題,我們能否按需定製自己所需的JDK呢,讓打的jar變的小一點。這也就是模組化的緣起。JDK 的模組來自Project Jigsaw,這個專案的主要目標為:

  • 使開發人員更容易構建維護庫和大型應用程式。
  • 提升Java SE平臺的安全性和可維護性,特別是對於JDK
  • 讓Java SE 和 JDK 能夠以更小的體積用於小型裝置和雲部署。

模組系統

那麼關於模組系統我們自然而然就有以下三個問題:

  • 模組系統的目標?
  • 什麼是模組?
  • 該如何使用模組?

模組系統的目標

  • Reliable configuration(依賴可配置):
模組化讓JVM可以識別依賴關係,不管是在編譯器還是執行時。系統根據這些依賴關係來確定程式所需要依賴的模組。
  • Strong encapsulation (強封裝)
模組的包只在模組顯式的將其匯出才可用,即使某個將其匯出(export),另一個模組如果不宣告需要(對應require), 那麼這個模組也不能使用匯出模組的包。這樣就提升了Java平臺的安全性, 因為模組潛在的訪問者更少,模組可以幫助開發者提出更簡潔、更合乎邏輯的設計。
  • Scalable Java platform (Java平臺的可擴充套件性)
以前,Java平臺是由一個大量包組成的整體,這給後續的開發和維護帶來了相當大的挑戰,現在Java平臺被模組化為95個模組(這個數字可能隨著Java的發展而改變)。現在您就可以自定義執行時,讓您的程式可以在執行時擁有僅它需要的模組。例如,如果您只想做服務端開發,不想要GUI模組,那麼在打包的時候,就可以不需要。從而顯著的減少執行時大小。

這裡我的解讀是我們平時會講職責單一,這一概念也應當不僅僅侷限於我們的類、方法,同時也應當上升到jar。就rt.jar來言,這個jar太大了,集合類、併發框架、awt和swing的部分類都在其中。rt是run time的縮寫,但是對於服務端的執行時一般不會用到這些類,從職責單一角度,awt和swing應當被劃分到桌面的jar裡面,現在放在rt.jar裡,這讓rt.jar顯得十分龐大。對此我想到的一個比喻是,rt.jar像是一個雜物間,放了太多不應該放的東西。現在模組化對其進行了分類整理,rt.jar被拆分。下面是JDK 9之後的模組化圖:

module-graph

現在最為核心的是java.base,職責更為單一,每個模組都宣告瞭自己依賴於哪些模組,原本這些可能放在文件中,現在這些依賴關係進入了程式碼。讓JDK平臺的可維護性得到了進一步的加強。上面這幅圖來自GitHub的module-graph。

  • Greater platform integrity(更高的平臺完整性)
在JDK 9之前,許多JDK的類是可以無限制的使用的,儘管對於設計這些的人來說,這些類並不是暴露給開發者使用的。現在透過模組化,再次進行了封裝,這些內部API被真正封裝。如果您的程式碼使用了這些內部API,升級到JDK9之上就會出現問題。

大多數是 sun.misc.*打頭的包中的類被隱藏,以Unsafe為例,sun.misc.Unsafe被移動到jdk.unsupported模組中,同時在java.base模組克隆了一個jdk.internal.misc.Unsafe類。jdk.internal包不開放給開發者使用。Unsafe在JDK 17上有了更好的替代者, 功能更強大,設計更為優秀,那就是MemorySegment,對於Java程式設計師來說MemorySegment更為友好。

什麼是模組?

模組在包之上增加了更高階別聚合,我個人覺得模組相對於jar來說多了描述說明和限制,以前我們看一個jar該如何使用的時候,往往要從文件看起,現在我們可以從模組的描述符來看起, 模組描述符給出了允許外部使用的類。模組由一個唯一命名的包、資源和一個模組描述符(一個java檔案)組成. 模組描述符指定了:

  • the module’s name:模組名稱
  • the module’s dependencies (that is, other modules this module depends on): 模組依賴項
  • the packages it explicitly makes available to other modules (all other packages in the module are implicitly unavailable to other modules) 允許哪些模組使用。
  • the services it offers 它提供的服務
  • the services it consumes 它消費的服務
  • to what other modules it allows reflection 允許哪些模組反射

如何使用?

單模組示例

我們講了那麼多理論,現在就來實踐一下, 注意模組化是JDK 9提供的特性,所以保證你的JDK版本要在8以上。我們本次演示的IDE是IDEA。

模組化例項01

我們基於JDK 11建了一個專案, module-info就是模組描述符:

module com.greetints {
    exports com.greetings;
}

module 關鍵字跟的是模組名, exports跟的是匯出的包。我們現在來將這個專案做成jar給外部使用:

匯出模組化jar

模組化示例2

模組化示例03

模組化示例04

模組化示例05

jar會出現在這裡:

模組化示例06

然後我們再用IDEA建立一個專案叫moduleDemo02:

moduleDemo02

然後在這個專案中將這個jar引進來:

模組化示例07

模組化示例08

模組化示例09

​ 一般來說在JDK9之前,到這裡我們就能用引入jar包的類了, 但是在JDK 9之後,對於模組後的jar,就用不了:

無法引入

我們需要在module-info裡,顯式的宣告一下我們需要使用引入的jar:

module moduleDemo02 {
    requires com.greetints;
}

多模組示例

exports to

IDEA一次只能開啟一個專案,Eclipse一次能開啟多個專案,但IDEA支援一個專案多個module,下面的module語法用多模組演示:

多module結構

moduleDemo01下面有兩個檔案:

package com.module01;

/**
 * @author xingke
 * @date 2022-12-11 15:39
 */
public class Module01 {

    public void sayHi(String msg){
        System.out.println(msg);
    }
}
module com.module01{
    exports com.module01 to com.module02;
}
exports package to module01,module02
代表匯出該模組下的包給module01,module02用
僅限module01,和module02用。其他模組無法使用

上面我們就在moduleDemo01裡面宣告瞭匯出com.module01包裡面的類給module02使用。我們這個專案裡面有三個module,那這就意味著在module03裡面無法使用com.module01裡面的類。儘管我們在moduleDemo03的檔案宣告瞭我們需要module01這個module,但是在IDEA中引入還是報錯:

不被允許使用

Module03裡面使用

但在module02裡面我們就可以使用, 在使用之前我們首先要在module-info裡面宣告一下:

引入module模組

ModuleDemo02示例

那曲線救國呢,現在moduleDemo02依賴module01,那我在moduleDemo03中引入可以使用moduleDemo01了嗎? 也是不可以的,因為我們上面使用的exports to語法限制了moduleDemo01的類僅能再moduleDemo02裡使用。那去掉這個限制呢,也是不行?那我該如何使用呢? 需要用到transitive關鍵字去宣告:

module com.module02 {
    requires transitive com.module01;
}
# 代表其他模組引入com.module02,也會將com.module01帶入

transitive 可以理解為傳遞依賴, 注意在JDK 9用public表達這個概念,JDK 11模組化被正式確立為永久特性,用transitive 表達。所以選擇JDK 學習的時候儘量選擇LTS版本。在JDK 裡面有這樣一個聚合模組叫java.se,這個模組將常用的模組透過transitive聚合在一起,我們引入此模組即能使用java.se開發所需的模組:

java.se

我們在moduleDemo03的module-info檔案中引入module02即可:

module com.module03 {
    requires com.module02;
}

opens…to

有同學看到上面可能會想到,雖然模組com.module01裡面宣告瞭只給com.module02使用,但是我用反射,我照樣也可以使用:

public class Module03 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Class.forName("com.module01.Module01");
        Object instance = clazz.getDeclaredConstructor().newInstance();
        for (Method method : clazz.getMethods()) {
            if (method.getName().contains("sayHi")){
                method.invoke(instance, "aaa");
            }
        }
    }
}

這種想法在JDK 9 之前是沒問題的,在JDK 9之後就會報這個錯:

無權訪問

去掉限制我們就可以用反射訪問com.module01的類,那如何對反射也進行控制呢? 這也就是opens to語法:

opens package to modulename

允許模組透過反射訪問包裡非public的方法。

示例:

module com.module01{
    exports com.module01;
    opens com.module01 to com.module02;
}

我們允許com.module02來透過反射訪問com.module01中的類裡非公開的方法, 如果沒有宣告,訪問非public級別的方法將會報下面的錯:

public class Module01 {
     void sayHi(String msg){
        System.out.println(msg);
    }
}

我們在module03裡面透過反射進行訪問:

public class Module03 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.module01.Module01");
        Object instance = clazz.getDeclaredConstructor().newInstance();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().contains("sayHi")){
                method.setAccessible(true);
                method.invoke(instance, "aaa");
            }
        }
    }
}

會報下面這個錯:

opens報錯

如果想允許模組下面的所有包的非public方法都可以透過訪問,那麼可以如下宣告:

open module com.module01{
    exports com.module01;
}

use 和 provides…with.

模組之間的橋樑通常會用介面來實現,這樣可以實現解耦,提供介面的時候同時提供實現類, 下面是示例:

首先我們要定義介面及其實現:

public interface SayMessage {
    void sayMessage();
}
// 注意這兩個實現不和sayMessage在一個包下,我們不對外暴露其實現
public class SayMessageImpl01 implements SayMessage {
    @Override
    public void sayMessage() {
        System.out.println("i'm SayMessageImpl01");
    }
}
public class SayMessageImpl02 implements SayMessage {
    @Override
    public void sayMessage() {
        System.out.println("i'm SayMessageImpl02");
    }
}

然後在module-info裡面宣告要暴露的服務:

module com.module01{
    exports com.module01;
    provides com.module01.SayMessage with com.module01.impl.SayMessageImpl01,com.module01.impl.SayMessageImpl02;
}

然後在moduleDemo02裡面使用moduleDemo01裡提供的服務,首先在moduleDemo02的module-info裡面宣告一下:

module com.module02 {
    requires   com.module01;
    uses com.module01.SayMessage;
}

然後我們在程式碼裡面使用即可:

public class ModuleDemo02 {
    public static void main(String[] args) {
        ServiceLoader<SayMessage> load = ServiceLoader.load(SayMessage.class);
        for (SayMessage sayMessage : load) {
            sayMessage.sayMessage();
        }
    }
}

load方法會自動呼叫SayMessage實現類的無參建構函式, 那這裡就會有同學問了,那我能讓他調有參的建構函式嗎?當然也是可以的,只不過要遵循一定的約定。我們首先改動SayMessageImpl02, 為其提供一個有參的建構函式:

public class SayMessageImpl01 implements SayMessage {

    private String name;

    @Override
    public void sayMessage() {
        System.out.println("i'm SayMessageImpl01"+name);
    }

    public SayMessageImpl01(String name) {
        this.name = name;
    }

    public static SayMessage provider(){
        SayMessage  sayMessage = new SayMessageImpl01("aa");
        return sayMessage;
    }
}

然後load在載入實現類的時候就自動的會呼叫Provider方法, 使用有參的構造。

模組相關的命令列選項

# 列出JDK目前的模組
java --list-modules 
# java --describe-module 模組名

我們看下java.xml的模組描述:

java.xml對外暴露的包

總結一下

模組系統讓Java輕裝出行,這也是趨勢,模組系統的到來讓JDK的執行時變的更小,可以讓我們定製執行時,也能更好的構建大型程式和庫。寫本篇的時候想起了一個小故事:

有兩條小魚快樂地遊著,碰到一條老魚從對面游過來。老魚向他們點頭問好:「早上好啊小夥子們,今天的水怎麼樣?」兩條小魚接著遊了一會兒,突然停了下來,一臉懵逼地看著對方:水是個什麼東西?

也許過幾年JDK 17全面普及,後面學習Java的人就會覺得模組化是理所當然的,就像水一樣,但對於老魚來說還是會問今天的水怎麼樣。

參考資料

相關文章