動態

詳情 返回 返回

JDK 新特性學習筆記之模塊系統 - 動態 詳情

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

習以為常的就是水

模塊系統是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的人就會覺得模塊化是理所當然的,就像水一樣,但對於老魚來説還是會問今天的水怎麼樣。

參考資料

  • What is Encapsulation in Java and How to Implement It https://www.simplilearn.com/t...
  • Java 面向對象特徵之一:封裝和隱藏 https://tiantianliu2018.githu...
  • Encapsulation is not information hiding https://www.infoworld.com/art...
  • Understanding Java 9 Modules https://www.oracle.com/corpor...
  • Everything You Need to Know About Polymorphism https://betterprogramming.pub...
  • 如何從 Java 8 升級到 Java 12,升級收益及問題處理技巧 https://www.infoq.cn/article/...*lm2h4h
  • Java 17 更新(9):Unsafe 不 safe,我們來一套 safe 的 API 訪問堆外內存 https://www.bennyhuo.com/2021...
  • 神奇的魔法類和雙刃劍-Unsafe https://cloud.tencent.com/dev...
  • Java模塊化開發,Java模塊系統精講 https://www.bilibili.com/vide...

Add a new 評論

Some HTML is okay.