前兩年重構一個遺留項目時,遇到了典型的"類路徑地獄"——項目依賴混亂,明明在類路徑裏的類卻報ClassNotFoundException,排查了半天才發現是不同版本的依賴衝突。後來用Java 9的模塊化(JPMS)重構後,模塊間依賴清晰可見,編譯期就能發現依賴問題,維護起來輕鬆多了。
Java 9引入的JPMS(Java Platform Module System)不僅解決了依賴混亂問題,還通過模塊邊界控制實現了更好的封裝。但很多開發者覺得模塊化複雜,尤其是模塊間通信和服務加載的機制。本文通過實際案例,講解如何定義模塊、實現模塊間通信,以及利用服務加載機制實現模塊解耦。
一、模塊化基礎:模塊定義與依賴
模塊化的核心是module-info.java文件,它定義了模塊的名稱、依賴和導出的包。我們先創建三個基礎模塊,演示最基本的模塊通信。
1. 創建基礎模塊
假設我們有三個模塊:
com.example.api:提供接口定義(API模塊)com.example.service:實現API的服務(服務模塊)com.example.app:使用服務的應用(應用模塊)
步驟1:定義API模塊(com.example.api)
在src/main/java下創建module-info.java:
// module-info.java
module com.example.api {
// 導出包含接口的包,允許其他模塊訪問
exports com.example.api;
}
創建接口類:
// com/example/api/UserService.java
package com.example.api;
public interface UserService {
String getUsername(Long id);
}
步驟2:定義服務模塊(com.example.service)
服務模塊依賴API模塊,並實現接口:
// module-info.java
module com.example.service {
// 聲明依賴API模塊
requires com.example.api;
// 導出服務實現包(如果需要被直接引用)
exports com.example.service.impl;
}
實現接口:
// com/example/service/impl/UserServiceImpl.java
package com.example.service.impl;
import com.example.api.UserService;
public class UserServiceImpl implements UserService {
@Override
public String getUsername(Long id) {
return "user_" + id;
}
}
步驟3:定義應用模塊(com.example.app)
應用模塊依賴服務模塊,使用其提供的實現:
// module-info.java
module com.example.app {
requires com.example.service;
}
應用主類:
// com/example/app/Main.java
package com.example.app;
import com.example.service.impl.UserServiceImpl;
import com.example.api.UserService;
public class Main {
public static void main(String[] args) {
UserService service = new UserServiceImpl();
System.out.println(service.getUsername(100L)); // 輸出 user_100
}
}
這種通過requires聲明依賴、exports導出包的方式,是模塊間通信的基礎。但它有個問題:應用模塊直接依賴了服務實現,耦合度高。如果要替換服務實現,必須修改應用代碼。
二、服務加載機制:實現模塊解耦
JPMS的服務加載機制(Service Loader)可以解決模塊耦合問題:應用模塊只依賴API,不依賴具體實現,通過服務接口動態加載實現類。
1. 改進模塊定義
步驟1:修改API模塊
API模塊只需要定義服務接口,無需修改:
// module-info.java(保持不變)
module com.example.api {
exports com.example.api;
}
步驟2:修改服務模塊
服務模塊不再導出實現類,而是通過provides...with聲明服務實現:
// module-info.java
module com.example.service {
requires com.example.api;
// 聲明提供UserService接口的實現(UserServiceImpl)
provides com.example.api.UserService with com.example.service.impl.UserServiceImpl;
}
實現類可以保持包私有(不被導出),增強封裝性:
// 包名可以不變,但無需被導出
package com.example.service.impl;
import com.example.api.UserService;
// 可以是包私有(不加public),僅模塊內可見
class UserServiceImpl implements UserService {
@Override
public String getUsername(Long id) {
return "user_" + id;
}
}
步驟3:修改應用模塊
應用模塊通過uses聲明使用的服務接口,無需依賴具體實現模塊:
// module-info.java
module com.example.app {
requires com.example.api;
// 聲明使用UserService服務
uses com.example.api.UserService;
}
應用通過ServiceLoader動態加載服務實現:
// com/example/app/Main.java
package com.example.app;
import com.example.api.UserService;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
// 加載所有實現UserService的服務
ServiceLoader<UserService> services = ServiceLoader.load(UserService.class);
// 獲取第一個可用的服務實現
UserService service = services.findFirst().orElseThrow();
System.out.println(service.getUsername(100L)); // 輸出 user_100
}
}
2. 服務加載的工作原理
- 服務模塊通過
provides聲明"我提供某個接口的實現"; - 應用模塊通過
uses聲明"我需要使用某個接口的服務"; - JVM在運行時自動查找所有提供該服務的模塊,通過
ServiceLoader加載實現類。
這種方式下,應用模塊和服務模塊完全通過API接口交互,實現瞭解耦。如果要替換服務實現,只需新增一個提供相同接口的服務模塊,無需修改應用代碼。
三、實戰進階:多服務實現與動態切換
實際開發中可能有多個服務實現(如MySQL實現、Oracle實現),服務加載機制支持動態選擇。
1. 新增服務實現模塊
創建com.example.service.mysql模塊,提供另一種實現:
// module-info.java
module com.example.service.mysql {
requires com.example.api;
provides com.example.api.UserService with com.example.service.mysql.MySQLUserService;
}
// 實現類
package com.example.service.mysql;
import com.example.api.UserService;
class MySQLUserService implements UserService {
@Override
public String getUsername(Long id) {
return "mysql_user_" + id;
}
}
2. 應用模塊動態選擇服務
應用可以遍歷所有服務實現,根據條件選擇:
public class Main {
public static void main(String[] args) {
ServiceLoader<UserService> services = ServiceLoader.load(UserService.class);
// 遍歷所有服務實現
for (UserService service : services) {
String username = service.getUsername(100L);
System.out.println("服務實現:" + service.getClass().getName() + ",結果:" + username);
}
// 根據實現類選擇
UserService mysqlService = services.stream()
.map(ServiceLoader.Provider::get)
.filter(s -> s.getClass().getName().contains("mysql"))
.findFirst()
.orElseThrow();
}
}
運行時如果同時包含com.example.service和com.example.service.mysql模塊,會輸出兩種實現的結果。這種動態發現機制非常適合插件化開發。
四、模塊邊界控制:exports與opens的區別
模塊間通信時,需要明確控制哪些類可以被訪問,exports和opens是兩種常用的訪問控制方式:
exports com.example.api:允許其他模塊在編譯期和運行時訪問該包的public類和成員;opens com.example.internal:只允許其他模塊在運行時通過反射訪問該包的類(如JSON序列化需要反射私有字段時使用)。
例如,服務實現中的內部包需要被JSON庫反射時:
// com.example.service的module-info.java
module com.example.service {
requires com.example.api;
requires com.fasterxml.jackson.databind; // 依賴Jackson
provides com.example.api.UserService with com.example.service.impl.UserServiceImpl;
// 允許Jackson在運行時反射訪問impl包
opens com.example.service.impl to com.fasterxml.jackson.databind;
}
五、模塊化常見問題與解決方案
- 找不到模塊依賴:確保
module-info.java中正確聲明requires,且模塊路徑(module-path)包含依賴的模塊JAR。 - 類可見性問題:如果編譯時報
package is not visible,檢查是否忘記exports對應的包,或依賴模塊是否正確requires當前模塊。 - 服務加載失敗:確保服務模塊用
provides...with聲明瞭實現,應用模塊用uses聲明瞭服務,且兩個模塊都在模塊路徑中。 - 反射訪問被拒絕:如果框架需要反射訪問模塊內的非導出類,用
opens聲明包,並指定允許訪問的模塊(如opens com.example.internal to spring.core)。
六、模塊化的適用場景
- 大型項目:通過模塊邊界控制依賴,減少類路徑衝突;
- 框架開發:用服務加載機制實現插件化(如日誌框架適配不同實現);
- 需要嚴格封裝的場景:隱藏內部實現,只暴露API接口。
小型項目如果依賴簡單,可能不需要模塊化,避免增加複雜度。
總結
Java模塊化通過module-info.java定義模塊依賴和邊界,解決了傳統類路徑的混亂問題。服務加載機制(provides/uses+ServiceLoader)則實現了模塊間的解耦,讓應用可以動態發現和使用服務實現,非常適合插件化和可擴展的系統。
實際開發中,不必一開始就將所有代碼模塊化,可以從核心模塊逐步遷移。關鍵是理解模塊邊界的控制(exports/opens)和服務加載的原理,這樣才能充分發揮JPMS的優勢,構建更清晰、更易維護的系統。