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