初識(shí)Java 9模塊化編程
原創(chuàng)【51CTO.com原創(chuàng)稿件】本文是Java9系列文章的***篇,我會(huì)通過(guò)幾篇文章系統(tǒng)性地介紹Java9的新特性。Java9的發(fā)布對(duì)于Java語(yǔ)言來(lái)說(shuō)是新的開始,希望Java能夠一直走下去,因?yàn)樗翘喑绦騿T賴以為生的編程工具。
一、模塊化問(wèn)題
我一直認(rèn)為,Java這門編程語(yǔ)言已經(jīng)不再僅僅是一門語(yǔ)言,它是一個(gè)由使用者根據(jù)自身工程需要所構(gòu)建起來(lái)的生態(tài)環(huán)境。既然是生態(tài)環(huán)境,它必然需要根據(jù)外部環(huán)境的變化不斷調(diào)整自己,不斷地吸收外部?jī)?yōu)良的設(shè)計(jì)方案,以用來(lái)不斷地加強(qiáng)自身,也必然需要不斷地改變擴(kuò)大自己的范圍,這樣也就不再僅僅局限于語(yǔ)言本身。
我們學(xué)習(xí)模塊化編程之前,應(yīng)該想想為什么Java需要引入模塊化設(shè)計(jì)思維?首先讓我們看看自己打包的jar文件。我們每天在構(gòu)建的應(yīng)用程序,也許大家編碼是基于某種框架,例如Spring Cloud,基于Spring Cloud可以很方便地啟動(dòng)微服務(wù)應(yīng)用,但是Spring Cloud背后引用了大量的Java生態(tài)環(huán)境里的第三方庫(kù)。
長(zhǎng)期來(lái)看,應(yīng)用程序如果缺乏結(jié)構(gòu)化設(shè)計(jì)思維,最終一定會(huì)付出代價(jià)?;氐絼偛诺膯?wèn)題,模塊化編程為什么會(huì)出現(xiàn)?因?yàn)樗某霈F(xiàn)可以讓我們更為簡(jiǎn)便、有效地管理庫(kù)之間的依賴關(guān)系,進(jìn)而減少了工程的復(fù)雜度。大家要問(wèn)了,不是有Maven可以實(shí)現(xiàn)這樣的功能嗎?對(duì)的,Maven確實(shí)可以,現(xiàn)在Java在JDK庫(kù)內(nèi)設(shè)計(jì)思維上吸收了Maven的設(shè)計(jì)思維優(yōu)點(diǎn),這樣JDK內(nèi)部也有了模塊化設(shè)計(jì)。
需要注意的是,模塊化編程并不是一下子出現(xiàn)的,它會(huì)出現(xiàn)首先是基于Java本身就是面向抽象編程的,也就是說(shuō),模塊化編程是構(gòu)建在抽象層之上的。相較于Java8引入的lambda表達(dá)式,模塊化編程關(guān)注的是整個(gè)應(yīng)用程序的結(jié)構(gòu)擴(kuò)展問(wèn)題,而lambda表達(dá)式更多的是提供了在一個(gè)類里面切換到lambda的方式。模塊化帶來(lái)的影響涉及到設(shè)計(jì)、編譯、打包、部署等等,所以我前面講了,它不僅僅是一個(gè)語(yǔ)言級(jí)的特性變化,它的意義比lambda表達(dá)式的引入大很多。
開始全文前***一個(gè)問(wèn)題,為什么JDK9一再推遲發(fā)布時(shí)間?JDK9的模塊化工程為了達(dá)到預(yù)期目標(biāo),即功能之間的邊界清晰目標(biāo),同時(shí)又要保持向后兼容、模塊內(nèi)定義良好且可讀,這些多重要求導(dǎo)致了JDK9的長(zhǎng)時(shí)間難產(chǎn)。這就好比我們編寫的應(yīng)用程序工程,通過(guò)20年的積累,成為了一個(gè)巨大無(wú)比的工程,這時(shí)候你再想把它按照模塊切分,難度就非常高了。
Java語(yǔ)言已經(jīng)積累了20年,現(xiàn)在才開始做模塊化設(shè)計(jì),其實(shí)是有點(diǎn)晚了,但是一旦做成了這件事情(JDK9的模塊化),后續(xù)的模塊化進(jìn)程就會(huì)變得快速起來(lái),這也就是為什么可能半年后你就會(huì)發(fā)現(xiàn)JDK10發(fā)布了。
二、模塊化編程
1. 什么是模塊化編程
什么是模塊化編程?模塊化編程是將原有的系統(tǒng)分解為若干個(gè)自己管理的模塊,但是這些模塊之間又是互相通信(連接)的。模塊,或者可以稱之為組件,也就成了一個(gè)個(gè)可以識(shí)別的獨(dú)立物件,它們可以包括代碼、元數(shù)據(jù)描述,以及和其他模塊之間的關(guān)系等。理想地看,這些物件從編譯時(shí)期開始就是可以被識(shí)別的,生命周期貫穿整個(gè)運(yùn)行時(shí)。這樣也就可以想象了,我們的應(yīng)用程序在運(yùn)行時(shí)應(yīng)該是由多個(gè)模塊組成的。
作為一個(gè)Java模塊,必須滿足三個(gè)基本要求:
(1) 強(qiáng)封裝性
對(duì)于封裝的重要性應(yīng)該不用和大家解釋了,兩個(gè)模塊之間僅需要知道對(duì)方的封裝接口、參數(shù)、返回值,而對(duì)于它內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),其他調(diào)用方并不關(guān)心,內(nèi)部怎么變化都沒關(guān)系,只要能夠繼續(xù)調(diào)用并返回正確的值就行。
(2) 定義良好的接口
這里包含兩層意思。一是模塊之間的邊界要?jiǎng)澐智宄?,不能存在重?fù)的部分,二是對(duì)于無(wú)法封裝的公開代碼,如果進(jìn)行了破壞性的修改,那么對(duì)其他調(diào)用方來(lái)說(shuō)也是破壞性的,因此需要提供定義良好并且穩(wěn)定的接口給其他調(diào)用模塊調(diào)用。
(3) 顯式依賴
這是點(diǎn)和面的關(guān)系。每一個(gè)點(diǎn)代表一個(gè)模塊,兩點(diǎn)之間的線代表模塊之間的依賴關(guān)系,所有點(diǎn)就組成了模塊調(diào)用關(guān)系圖。只有擁有清晰的模塊調(diào)用關(guān)系圖,我們才能確保調(diào)用關(guān)系的正確性和模塊配置的可用性。Java9之前,我們可以采用maven來(lái)幫助管理外部依賴關(guān)系。
模塊化帶來(lái)的是靈活、可理解、可重用這三大優(yōu)點(diǎn)。模塊化編程和當(dāng)今很多軟件架構(gòu)概念是類同的,都是為了解決相似的抽象層問(wèn)題,例如基于組件的開發(fā)、面向服務(wù)系統(tǒng)架構(gòu),或者更新的微服務(wù)架構(gòu)。
前面提到了三個(gè)基本要求,強(qiáng)封裝性、定義良好的接口、顯式依賴,其實(shí)在Java9之前就已經(jīng)支持了。比如封裝,類型的封裝可以通過(guò)使用包和訪問(wèn)修飾符(例如public、protected、private)的組合方式完成。例如protected,只有在同一個(gè)包內(nèi)的類才能訪問(wèn)protected類里面的方法。這里你就可以提出一個(gè)問(wèn)題來(lái)了。
如果我們想要讓一些包外的類可以訪問(wèn)protected類,又不想讓另外一些包外的類可以訪問(wèn),這時(shí)候應(yīng)該怎么處理呢?Java9之前沒有很好的解決方案。對(duì)于第二個(gè)要求,定義良好的接口,這一點(diǎn)Java語(yǔ)言一直做得不錯(cuò),從一開始就做得不錯(cuò)。你會(huì)發(fā)現(xiàn)接口方式在整個(gè)模塊化編程中扮演了中心角色。
對(duì)于顯式依賴,由于Java提供的import關(guān)鍵字所引入的jar包需要在編譯時(shí)才會(huì)真正加載,當(dāng)你把代碼打入jar包的時(shí)候,你并不知道哪一個(gè)jar文件包含了你的jar包需要運(yùn)行的類型。為了解決這個(gè)問(wèn)題,我們可以利用一些外部工具,例如Maven、OSGi。Java9雖然從jvm核心層和語(yǔ)言層解決了依賴控制問(wèn)題,但是Maven、OSGi還是有用武之地的,它們可以基于Java模塊化編程平臺(tái)之上繼續(xù)持續(xù)自己的依賴管理工作。
圖1:應(yīng)用程序的jar包關(guān)系圖
上面這張圖包含了兩個(gè)部分,一部分是應(yīng)用程序,包含Application.jar的應(yīng)用程序jar包、該jar包的兩個(gè)依賴庫(kù)(Google Guava和Hibernate Validator),以及三個(gè)外部依賴jar包。我們可以通過(guò)maven工具完成庫(kù)之間的依賴關(guān)系綁定功能。
Java9出現(xiàn)之前,Java運(yùn)行時(shí)還需要包含rt.jar,如上圖所示。從這張圖上至少可以看出沒有強(qiáng)有力的封裝概念,為什么這么說(shuō)?以Guava庫(kù)為例,它內(nèi)部的一些類是真的需要被Application.jar工程使用的,但是有一些類是不需要被使用的,但是由于這些類的訪問(wèn)限制符也是public的,所以外部包里的類是可以訪問(wèn)到的,所以說(shuō)沒有履行Java9的封裝要求。
大家知道,當(dāng)JVM開始加載類時(shí),采用的方式是順序讀取classpath里面設(shè)置的類名并找到需要的類,一旦找到了正確的類,檢索工作結(jié)束,轉(zhuǎn)入加載類過(guò)程。那么如果classpath里面沒有需要的類呢?那就會(huì)拋出運(yùn)行時(shí)錯(cuò)誤(run-time exception)。又由于JVM采用的延遲加載方式(lazy loading),因此極有可能某個(gè)用戶點(diǎn)了某個(gè)按鈕,然后就奔潰了,這是因?yàn)镴VM不會(huì)從一開始就有效地驗(yàn)證classpath的完整性。那么,如果classpath里面存在重復(fù)的類,會(huì)出現(xiàn)什么情況呢?可能會(huì)出現(xiàn)很多莫名其妙的錯(cuò)誤,例如類的方法找不到,這有可能是因?yàn)榕渲昧藘蓚€(gè)不同版本的jar包。
2. 模塊化系統(tǒng)目標(biāo)
Java9的模塊化系統(tǒng)有兩大目標(biāo):
- 模塊化JDK本身;
- 為應(yīng)用程序的使用提供模塊化系統(tǒng)。
模塊化系統(tǒng)為Java語(yǔ)言和運(yùn)行時(shí)環(huán)境引入了本地模塊化概念,提供了強(qiáng)有力的封裝。
如圖1所示,在Java9之后,每一個(gè)jar包都變成了一個(gè)模塊,包括引用其他模塊的顯示依賴。從圖1可以知道,Application調(diào)用了JDK的Java.sql包。
圖2:應(yīng)用程序的模塊化調(diào)用關(guān)系
圖3描述的是JDK內(nèi)部的模型化系統(tǒng)(JSR376和JEP261),已經(jīng)被合并到了JDK9。
圖3:JDK內(nèi)部的模型化系統(tǒng)圖
從圖3大家可以知道,各個(gè)模塊之間有著千絲萬(wàn)縷的引用關(guān)系,但是要記住,JDK9的模塊化設(shè)計(jì)做得很精巧,它僅僅允許單一方向(向下)引用,不允許出現(xiàn)環(huán)形結(jié)構(gòu),這樣可以確保引用關(guān)系圖的簡(jiǎn)單設(shè)計(jì)原則。
3. 模塊化的JDK
在Java模塊化系統(tǒng)引入之前,JDK的運(yùn)行時(shí)庫(kù)包括了重量級(jí)的rt.jar,該jar文件的總大小超過(guò)60M,包含了大量的運(yùn)行時(shí)類。為了重構(gòu)整個(gè)Java平臺(tái),也為了Java能夠在輕量級(jí)語(yǔ)言解決方案越來(lái)越占主導(dǎo)地位的情況下讓Java語(yǔ)言繼續(xù)保持旺盛的生命力,JDK團(tuán)隊(duì)引入了JDK的模塊化設(shè)計(jì),這個(gè)決定可能是關(guān)鍵性的。
在過(guò)去的20年中,JDK的若干次發(fā)布,每一次都會(huì)包含許多新的特性,因此也增加了大量的類。以CORBA為例,它在上世紀(jì)90年代的時(shí)候被認(rèn)為是企業(yè)級(jí)計(jì)算的未來(lái),當(dāng)然現(xiàn)在幾乎沒有人記得它了,然而用于支持CORBA的類仍然被包含在rt.jar包里面,也就是說(shuō),無(wú)論你有沒有用到這些類,只要你的應(yīng)用程序是分布式的,你都不得不帶著它們一起運(yùn)行。這樣做的直接后果是浪費(fèi)了磁盤空間、內(nèi)存空間,以及CPU資源(需要增大CPU運(yùn)行耗時(shí))。對(duì)于資源受限的硬件載體,或者云端的資源,這樣就產(chǎn)生了浪費(fèi),也增加了成本(云端資源是按需申請(qǐng)的,能省就省)。
那么我們可不可以直接移除這些不需要的類呢?不能這么簡(jiǎn)單執(zhí)行,因?yàn)槲覀冃枰紤]每次發(fā)布之后的向前兼容,直接刪除API會(huì)導(dǎo)致JDK升級(jí)后一些老的應(yīng)用程序不可用。JDK引入模塊化管理方式后,我們只需要忽略包含CORBA的模塊就可以了。
當(dāng)然,分解單體型(monolithic)的JDK并不僅僅是移除過(guò)時(shí)(例如CORBA)的類。JDK包含的很多技術(shù)都是這樣的,對(duì)于一些人有用,對(duì)于另一些人則是無(wú)用的,但是并不是說(shuō)它們過(guò)時(shí)了,僅僅是應(yīng)用程序不需要使用。Java語(yǔ)言一直以來(lái)就存在安全性漏洞,通過(guò)模塊化設(shè)計(jì)可以減少類的應(yīng)用,自然也就降低了漏洞發(fā)生的幾率。
截止目前,JDK9大約有超過(guò)90個(gè)平臺(tái)模塊,這種方式取代了以往的單一型大庫(kù)形態(tài)。平臺(tái)模塊是JDK的一部分,它和應(yīng)用程序模塊是不一樣的,應(yīng)用程序模塊是由程序員自己創(chuàng)建的。但是從技術(shù)層面來(lái)看,平臺(tái)模塊和應(yīng)用程序模塊又沒有什么區(qū)別。每一個(gè)平臺(tái)模塊構(gòu)造了一個(gè)JDK功能,從日志到XML的支持,等等,覆蓋了原有單一型JDK的功能。
在JDK9里,所有的模塊都需要在外部顯示地定義與其他模塊之間的依賴關(guān)系,這就好比我們買可拆裝家具時(shí)的各模塊之間的榫頭,你一看就知道需要和哪些其他模塊進(jìn)行拼接,而一些其他模塊都可以拿來(lái)公用的模塊,比如java.logging,你就會(huì)發(fā)現(xiàn)很多模塊都會(huì)應(yīng)用它。也正是由于引入了模塊化,JDK內(nèi)部終于在各個(gè)模塊之間有了清晰的界限,互相的引用關(guān)系終于清晰了。
注意,按照J(rèn)DK9目前的模塊化設(shè)計(jì)理念,所有的依賴關(guān)系都是指向向下方向的,不會(huì)出現(xiàn)編譯時(shí)的各模塊間環(huán)形依賴情況,你自己編寫的應(yīng)用程序模塊也需要避免這種情況發(fā)生。
4. 模塊資源介紹
一個(gè)模塊包含模塊名稱、相關(guān)的代碼和資源,這些都被保存在稱為module-info.java的模塊描述文件里,以下面這個(gè)文件為例,描述java.prefs平臺(tái)模塊。
(1) 清單1 module-info.java
- module java.prefs{
- requires java.xml;
- exports java.util.prefs;
- }
代碼清單1內(nèi)包含了requires和exports兩個(gè)關(guān)鍵字,逐一解釋:
- requires關(guān)鍵字表示了依賴關(guān)系,這里明確模塊需要依賴java.xml模塊,如果沒有依賴生命,java.prefs模塊在編譯時(shí)會(huì)拒絕執(zhí)行編譯命令。這一點(diǎn)是向Maven借鑒的,使用前必須聲明才能使用。
- exports關(guān)鍵字表示了其他模塊如何可以引用java.prefs包,由于模塊化編程已經(jīng)把強(qiáng)封裝性設(shè)置成了默認(rèn)選項(xiàng),因此只有當(dāng)包被顯式地聲明導(dǎo)出(就是這里的exported),導(dǎo)出為本例的java.util.prefs包。Exports是針對(duì)原有的訪問(wèn)方式(public、protected、private)的一個(gè)補(bǔ)充,是針對(duì)強(qiáng)一致性的補(bǔ)充,Java9之后,public僅僅是針對(duì)模塊內(nèi)部類之間的訪問(wèn)權(quán)限,如果你想要從外部能夠應(yīng)用模塊內(nèi)部類,你必須要exports。
注意,模塊名由于是全局變量,所以需要是全局唯一的。
5. HelloWorld案例
接下來(lái)簡(jiǎn)單介紹一個(gè)HelloWorld示例。如清單2所示,HelloModularWorld類的main函數(shù)負(fù)責(zé)打印字符串“Hello World, new modular World!”。
(2) 清單2 HelloModularWorld類
- package org.michael.demo.jpms;
- public class HelloModularWorld {
- public static void main(String[] args) {
- System.out.println("Hello World, new modular World!");
- }
- }
為了實(shí)現(xiàn)模塊化,需要在工程的根目錄下創(chuàng)建一個(gè)名為module-info.Java的類,內(nèi)容如清單3所示:
(3) 清單3 module-info.Java源碼
- module org. michael.demo.jpms_hello_world {
- // this module only needs types from the base module 'Java.base';
- // because every Java module needs 'Java.base', it is not necessary
- // to explicitly require it - I do it nonetheless for demo purposes
- requires Java.base;
- // this export makes little sense for the application,
- // but once again, I do this for demo purposes
- exports org.michael.demo.jpms;
- }
如代碼清單3所示,引用了Java.base,輸出至org.michael.demo.jpms包。接下來(lái)開始編譯,如清單4所示。
(4) 清單4 編譯模塊化系統(tǒng)
- $ Javac
- -d target/classes
- ${source-files}
- $ jar --create
- --file target/jpms-hello-world.jar
- --main-class org.michael.demo.jpms.HelloModularWorld
- -C target/classes .
- $ Java
- --module-path target/jpms-hello-world.jar
- --module org. michael.demo.jpms_hello_world
就這個(gè)簡(jiǎn)單的示例來(lái)看,除了增加了一個(gè)文件、編譯時(shí)的差別替換為使用模塊路徑方式(module path),以及工程沒有了manifest文件以外,其他和Java9之前的編程/編譯方式是一樣。
三、結(jié)束語(yǔ)
本文主要介紹了什么是Java9模塊化編程。首先從Java9為什么遲遲不能發(fā)布說(shuō)起,然后引申出什么是模塊化編程,接著系統(tǒng)性地介紹模塊化編程的系統(tǒng)目標(biāo)、特點(diǎn)、要求,再通過(guò)JDK的模塊化案例介紹,讓讀者能夠了解JDK的發(fā)展趨勢(shì)。***,通過(guò)一個(gè)HelloWorld實(shí)例讓讀者能夠深入淺出地了解Java模塊化編程。下一篇文章我會(huì)介紹模塊化對(duì)應(yīng)的服務(wù)和模式。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】
【本文為51CTO專欄作者“周明耀”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】




























