(全文目錄:)
開篇語
哈嘍,各位小夥伴們,你們好呀,我是喵手。運營社區:C站/掘金/騰訊雲/阿里雲/華為雲/51CTO;歡迎大家常來逛逛
今天我要給大家分享一些自己日常學習到的一些知識點,並以文字的形式跟大家一起交流,互相學習,一個人雖可以走的更快,但一羣人可以走的更遠。
我是一名後端開發愛好者,工作日常接觸到最多的就是Java語言啦,所以我都儘量抽業餘時間把自己所學到所會的,通過文章的形式進行輸出,希望以這種方式幫助到更多的初學者或者想入門的小夥伴們,同時也能對自己的技術進行沉澱,加以覆盤,查缺補漏。
小夥伴們在批閲的過程中,如果覺得文章不錯,歡迎點贊、收藏、關注哦。三連即是對作者我寫作道路上最好的鼓勵與支持!
前言
我以前一直覺得 Java 模塊系統(JPMS)像個“強迫症工具”:你不寫 module-info.java 吧,它説你不規範;你寫了吧,它又開始管你反射、管你訪問、管你動態加載……直到有一天我在做插件系統,親眼看到“類加載混戰 + 依賴衝突 + 反射亂飛”把服務折騰得雞飛狗跳,才突然明白:**模塊不是來折磨人的,是來讓系統有邊界的。**😅 而“層(Layer)”就是把這個邊界再往前推進一步:**不僅要分模塊,還要分“模塊世界”。**這章我們就把它講透,順帶給一個能跑起來的“分層模塊框架”骨架。
I. 模塊層:ModuleLayer 和 Controller
在 JPMS 裏,模塊不是散落在 classpath 上的 jar,而是存在於一個“模塊圖(module graph)”裏:誰讀誰(requires),誰暴露包(exports),誰開放反射(opens)。 而 ModuleLayer 可以理解為:一個完整的模塊圖 + 對應的類加載環境。
ModuleLayer.boot():JVM 啓動時的“默認層”,也就是你應用本體所在的模塊世界- 自定義 Layer:你可以在運行時創建新的層,把插件模塊放進去
ModuleLayer.Controller 是幹嘛的?
Controller 像是“層的管理手柄”: 它允許你在運行時對層裏的模塊做一些受控調整,比如:
- 給某個模塊 額外 addOpens / addExports(只對特定模塊生效)
- 在插件場景裏,這比 JVM 參數
--add-opens更“精確”,也更像工程
換句話説:
ModuleLayer負責“搭世界”,Controller負責“給世界開個小門”。
II. 動態模塊:defineModulesWithOneLoader
插件要動態加載,你就得在運行時把一堆模塊“裝進一個新 Layer”。常見路徑是:
- 找到插件模塊的 jar(或目錄)
- 用
ModuleFinder找模塊描述 - 基於父層(通常是 boot layer)做
resolve得到新配置 - 用
ModuleLayer.defineModulesWithOneLoader(...)把模塊定義到新層,並指定一個 ClassLoader 來加載它們
為什麼是 “WithOneLoader”?
因為這是最容易上手、也最常見的一種策略:
- 一個 Layer 用一個 ClassLoader 裝它的模塊
- 插件之間要隔離,就建多個 layer(每個插件一個 layer)
- 插件要共享依賴,就讓它們在同一個 layer 或共享父 layer 的模塊
這比傳統“所有插件都丟一個 URLClassLoader”更可控:模塊可讀性、包導出、反射開口都能被 JPMS 管起來。
III. 反射訪問:addOpens 和 addExports
模塊系統最讓人抓頭髮的點,往往不是 “requires”,而是:“為啥我反射突然不行了?” 原因很簡單:在模塊世界裏,訪問被分成兩條線:
1) exports:編譯期/運行期的“普通訪問”
- 允許別的模塊
import這個包裏的 public 類型 - 是“代碼級訪問”
2) opens:反射訪問(深反射)
- 允許別的模塊通過反射訪問(包括 private 成員、setAccessible 等相關行為)
- 是“反射級訪問”
- 常用於:DI 容器、序列化框架、ORM、代理增強
結論很直白:
- 你要讓別的模塊“用你的 API” →
exports - 你要讓框架“反射你內部” →
opens
3)運行時加門:addOpens / addExports
在插件/框架場景裏,你可能不想把門寫死在 module-info.java(畢竟開放過頭很危險)。這時就輪到 ModuleLayer.Controller 出場了:對 特定目標模塊 開門,而不是對全世界開門。
注意:這類運行時開門需要權限與邊界設計,別把它當“萬能鑰匙”。🙂
IV. 多層架構:父子層關係
Layer 是有父子關係的,這一點非常關鍵。你可以把它想成“繼承模塊世界的基礎設施”:
- 父層提供基礎模塊與類(例如應用核心模塊、公共 API 模塊)
- 子層加載插件模塊,並且可以讀父層的一些模塊(取決於你的配置與模塊聲明)
常見架構(很實用)
-
Boot Layer:應用主體 + 公共 API
-
Plugin Layer(N 個):每個插件一個 layer
- 插件模塊
requires公共 API 模塊 - 插件模塊自己帶實現與依賴(可隔離)
- 插件模塊
這帶來一個很現實的收益:
插件 A 的依賴版本和插件 B 的依賴版本可以不一樣,只要它們不把衝突帶回父層。
V. 使用場景:插件和動態加載
“Layer + 動態模塊”最有價值的地方,就是插件系統。你通常會遇到這些痛點:
- 依賴衝突:插件各帶一套依賴,版本不一致
- 隔離需求:插件不應該隨便反射/訪問主程序內部
- 可卸載/可替換:插件更新不希望重啓整個系統
- 安全邊界:插件的權限要可控(至少在模塊級別可控)
JPMS + Layer 做不到“一鍵安全沙箱”,但它能把“誰能訪問誰”這件事從約定變成機制。 説人話:以前靠自覺,現在靠規則。
VI. 示例:分層模塊的應用框架(一個能落地的骨架)
下面給你一個“最小但夠工程味”的分層插件框架結構:
- 主程序:
app.host - 公共 API:
app.api - 插件實現:
plugin.hello
説明:這裏給的是核心代碼骨架,讓讀者能按這個思路搭項目。真實項目還會加插件目錄掃描、版本管理、生命週期、隔離策略、權限策略等。
1)公共 API 模塊:app.api
module-info.java
module app.api {
exports com.example.api;
}
SPI 接口:
package com.example.api;
public interface Plugin {
String name();
String execute(String input);
}
2)主程序模塊:app.host
module-info.java
module app.host {
requires app.api;
// 主程序使用 ServiceLoader 來發現插件實現
uses com.example.api.Plugin;
}
Host 側:動態創建插件 Layer 並加載服務
package com.example.host;
import com.example.api.Plugin;
import java.lang.module.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.*;
public class PluginHost {
public static void main(String[] args) throws Exception {
// 假設插件 jar 在 plugins/plugin.hello.jar
Path pluginJar = Path.of("plugins/plugin.hello.jar");
// 1) 找到插件模塊
ModuleFinder finder = ModuleFinder.of(pluginJar);
// 2) 父層配置(通常是 boot layer)
ModuleLayer parent = ModuleLayer.boot();
Configuration parentConfig = parent.configuration();
// 3) 解析:把 finder 裏的模塊解析進新配置
// 這裏需要指定根模塊名(插件 module name)
String pluginModuleName = "plugin.hello";
Configuration pluginConfig = parentConfig.resolve(finder, ModuleFinder.of(), Set.of(pluginModuleName));
// 4) 為插件層準備 classloader(一個 layer 一個 loader 的典型策略)
// 注意:真實項目裏會更精細地控制 URL、權限、隔離
URL[] urls = { pluginJar.toUri().toURL() };
ClassLoader pluginLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
// 5) 定義新層
ModuleLayer pluginLayer = parent.defineModulesWithOneLoader(pluginConfig, pluginLoader);
// 6) 在插件層裏用 ServiceLoader 找到實現
ServiceLoader<Plugin> loader = ServiceLoader.load(pluginLayer, Plugin.class);
for (Plugin p : loader) {
System.out.println("Loaded plugin: " + p.name());
System.out.println("Result: " + p.execute("hello from host"));
}
}
}
這段代碼的“工程意義”在於:
- 插件模塊被裝入一個單獨 Layer(隔離的模塊世界)
- 插件可讀
app.api(通過 requires) - Host 通過
ServiceLoader從指定 Layer 加載服務實現(避免誤掃 classpath)
3)插件模塊:plugin.hello
module-info.java
module plugin.hello {
requires app.api;
provides com.example.api.Plugin with com.example.plugin.HelloPlugin;
}
插件實現:
package com.example.plugin;
import com.example.api.Plugin;
public class HelloPlugin implements Plugin {
@Override
public String name() {
return "hello-plugin";
}
@Override
public String execute(String input) {
return "plugin says: [" + input + "]";
}
}
4)反射訪問的“受控開門”示意(addOpens / addExports)
假設插件框架需要反射訪問 host 某個內部包(通常我不建議輕易這麼做,但現實裏確實會遇到,比如序列化、注入、綁定等),你應該儘量做到:
- 只對特定插件模塊開門
- 只開放必要的包
- 最好能配置化並審計
概念代碼(説明 Controller 的用途):
// parent.defineModules... 返回的是 layer,不直接暴露 controller
// 更復雜的 defineModules... API 允許你拿到 controller 並做 addOpens/addExports
// 這裏用“表達意圖”的偽示例説明:
// controller.addOpens(hostModule, "com.example.host.internal", pluginModule);
// controller.addExports(hostModule, "com.example.host.spi", pluginModule);
實踐建議:
- 如果你必須開放反射:優先在
module-info.java用opens ... to plugin.module精確開放(最可審計、最穩定) - 運行時開門作為“動態治理手段”,不要變成默認依賴
一些“踩過坑的人才會在意”的建議
這段我説得直一點,都是實戰裏很容易翻車的點:
-
插件不要隨便讀 host 模塊: host 應儘量只暴露
app.api,別讓插件“穿透到內部實現”,否則你以後重構 host 會被插件綁架。 -
ServiceLoader 的 Layer 要明確:
ServiceLoader.load(Plugin.class)默認走當前 layer/classpath,插件隔離後容易“加載不到/加載錯”。用ServiceLoader.load(pluginLayer, Plugin.class)更穩。 -
類加載器策略要可解釋: 一層一 loader 好理解也好排障;想做更復雜的共享依賴,需要更明確的依賴分層,不然排錯像抓鬼。
-
反射開門要最小化:
opens一開,框架很爽,安全邊界就鬆了。能不開放就不開放;必須開放就“定向 opens to”。 -
卸載不是自動的: Layer/ClassLoader 是否能被回收,取決於你有沒有強引用(緩存了 Class、MethodHandle、單例等)。別指望“關掉插件就自動卸載”,要設計生命週期與清理策略。
小結:Layer 讓“動態加載”從技巧變成結構
如果把模塊系統比作“城市規劃”,那 Layer 就是“分城運營”:
- 主城(boot layer)保持穩定與邊界
- 插件在分城(child layers)自由迭代
- 需要交流就走明面通道(exports/uses/provides)
- 需要特殊通行證才開小門(opens/addOpens)
最後我想用一句反問收尾(我這人嘴欠,但這句真的好用😄): 你的插件到底應該“依賴你的實現”,還是隻應該“依賴你的契約”? 如果答案是前者,那你遲早會被自己的系統鎖死;如果是後者,Layer 才真正發揮價值。
... ...
文末
好啦,以上就是我這期的全部內容,如果有任何疑問,歡迎下方留言哦,咱們下期見。
... ...
學習不分先後,知識不分多少;事無鉅細,當以虛心求教;三人行,必有我師焉!!!
wished for you successed !!!
⭐️若喜歡我,就請關注我叭。
⭐️若對您有用,就請點贊叭。 ⭐️若有疑問,就請評論留言告訴我叭。
版權聲明:本文由作者原創,轉載請註明出處,謝謝支持!