1. 什麼是 module-info.java,它在哪裡?
在 Java 中,模組一律從檔案 module-info.java 開始。它相當於模組的「護照」,你在其中宣告其名稱、要匯出的套件,以及對其他模組的相依。
在哪裡找?
檔案 module-info.java 必須位於該模組原始碼資料夾的根目錄。比如,若你的專案結構如下:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
這裡 my.module.name 是模組名稱(命名規則稍後說明)。
沒有這個檔案,模組就不算是模組!
若缺少它,無論有多少資料夾與類別,那都只是一般的「舊式」Java 專案。
2. module-info.java 語法:基本元素
先直接看看最簡單的檔案範例——真的一點也不可怕!
module my.module.name {
exports com.example.api;
requires java.sql;
}
逐段拆解:
module my.module.name { ... }
這是在宣告名為 my.module.name 的模組。模組名稱是唯一識別子,通常與根套件一致(例如 com.example.app)。
有趣的事實:如果你把模組命名為 java.base,編譯器會很不高興。別嘗試偽裝標準模組。
exports com.example.api;
這一行表示:「我這個模組願意公開套件 com.example.api 內的一切。」在該套件中標記為 public 的內容,對其他模組可見。其他內容則僅供本模組內部使用。
requires java.sql;
在這裡我們坦承告訴編譯器:「我需要標準模組 java.sql 才能運作。」沒有這段宣告,編譯器不允許我們使用該模組中的類別。
其他關鍵字(拓展知識)
- opens <package>; — 將套件對反射開放(例如供 Jackson 這類序列化函式庫使用)。
- uses <service-interface>; — 表示此模組使用某個服務(介面)。
- provides <service-interface> with <implementation-class>; — 表示此模組提供服務的實作。
本講次我們聚焦在 exports 與 requires——它們在絕大多數教學與實務專案中都會用到。
3. module-info.java 範例
範例 1:最小模組
module com.example.hello {
exports com.example.hello.api;
}
- 此模組只匯出套件 com.example.hello.api。
- 像是位於 com.example.hello.internal 的內容都會對其他模組隱藏,即使裡面有 public 類別也一樣。
範例 2:具有相依性的模組
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
我們之所以能使用 JDBC,是因為明確宣告了對 java.sql 的相依。
範例 3:匯出多個套件
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
可以匯出任意多個套件(但別什麼都匯出——這正是模組的意義!)。
4. 限制與規則
模組名稱
- 通常與根套件一致(例如,com.example.app)。
- 不可與標準模組名稱相同(java.base、java.sql 等)。
- 不得包含空白或特殊字元,亦不得以數字開頭,等等。
- 建議:使用貴組織或專案的反向網域名稱以避免衝突。
一個模組——僅一個 module-info.java
每個模組只能有一個這樣的檔案。要是有兩個——編譯器就會讓你「模組大亂」。
同一套件只能由一個模組匯出
同一個套件不能由兩個不同的模組同時匯出。這就像拿同一名字辦兩本護照——國家可不會同意。
模組內的套件
只能匯出在該模組原始碼結構中實際存在的套件。匯出不存在的套件會導致編譯錯誤。
5. 實作:在專案中建立 module-info.java
假設我們有一個簡單專案,其內容如下:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
步驟 1:建立 module-info.java
module com.example.greetings {
exports com.example.greetings.api;
}
步驟 2:嘗試在另一個模組使用 internal 套件中的類別
假設我們有第二個模組 com.example.app,想要存取 SecretSauce:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // 錯誤!
結果:
編譯器會說:「套件 com.example.greetings.internal 未由模組 com.example.greetings 匯出。」即使類別 SecretSauce 是 public,它也無法被其他模組存取。
這就是模組層級的真正封裝!
步驟 3:試著不宣告 requires
如果在 com.example.app 中未寫上 requires com.example.greetings;,卻嘗試使用 com.example.greetings.api 中的類別,編譯器會拋出錯誤:
package com.example.greetings.api is not visible
6. 使用 module-info.java 時的常見錯誤
錯誤 №1:模組名稱與專案結構不相符。
如果你把模組命名為 com.example.app,而資料夾結構卻是 src/main/java/app,編譯器會不知所措。模組名稱通常與根套件一致,資料夾結構也應加以反映。
錯誤 №2:凡事都匯出。
只匯出確實需要讓其他模組看見的部分。不要為了「比較簡單」就直接 exports com.example;。這會破壞封裝。
錯誤 №3:忘了加入 requires.
若你使用了其他模組或標準函式庫中的類別(例如,java.sql),卻忘了宣告相依——就會發生編譯錯誤。
錯誤 №4:在 exports 中匯出不存在的套件。
如果你寫了 exports com.example.foo;,但實際沒有該套件——編譯器會指出你在「匯出空氣」。
錯誤 №5:非匯出套件中的 public 類別.
若類別宣告為 public,卻位於未被匯出的套件中,那麼它只能在模組內部可見。這不算錯誤,但常讓新手感到意外。
錯誤 №6:同一模組中包含多個 module-info.java。
一個模組只能有一個檔案 module-info.java。若有兩個——編譯器將無法建置專案。
GO TO FULL VERSION