(全文目錄:)

開篇語

哈嘍,各位小夥伴們,你們好呀,我是喵手。運營社區:C站/掘金/騰訊雲/阿里雲/華為雲/51CTO;歡迎大家常來逛逛

  今天我要給大家分享一些自己日常學習到的一些知識點,並以文字的形式跟大家一起交流,互相學習,一個人雖可以走的更快,但一羣人可以走的更遠。

  我是一名後端開發愛好者,工作日常接觸到最多的就是Java語言啦,所以我都儘量抽業餘時間把自己所學到所會的,通過文章的形式進行輸出,希望以這種方式幫助到更多的初學者或者想入門的小夥伴們,同時也能對自己的技術進行沉澱,加以覆盤,查缺補漏。

小夥伴們在批閲的過程中,如果覺得文章不錯,歡迎點贊、收藏、關注哦。三連即是對作者我寫作道路上最好的鼓勵與支持!

前言

  我以前一直覺得 Java 模塊系統(JPMS)像個“強迫症工具”:你不寫 module-info.java 吧,它説你不規範;你寫了吧,它又開始管你反射、管你訪問、管你動態加載……直到有一天我在做插件系統,親眼看到“類加載混戰 + 依賴衝突 + 反射亂飛”把服務折騰得雞飛狗跳,才突然明白:**模塊不是來折磨人的,是來讓系統有邊界的。**😅 而“層(Layer)”就是把這個邊界再往前推進一步:**不僅要分模塊,還要分“模塊世界”。**這章我們就把它講透,順帶給一個能跑起來的“分層模塊框架”骨架。

I. 模塊層:ModuleLayerController

  在 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”。常見路徑是:

  1. 找到插件模塊的 jar(或目錄)
  2. ModuleFinder 找模塊描述
  3. 基於父層(通常是 boot layer)做 resolve 得到新配置
  4. ModuleLayer.defineModulesWithOneLoader(...) 把模塊定義到新層,並指定一個 ClassLoader 來加載它們

為什麼是 “WithOneLoader”?

因為這是最容易上手、也最常見的一種策略:

  • 一個 Layer 用一個 ClassLoader 裝它的模塊
  • 插件之間要隔離,就建多個 layer(每個插件一個 layer)
  • 插件要共享依賴,就讓它們在同一個 layer 或共享父 layer 的模塊

這比傳統“所有插件都丟一個 URLClassLoader”更可控:模塊可讀性、包導出、反射開口都能被 JPMS 管起來。

III. 反射訪問:addOpensaddExports

  模塊系統最讓人抓頭髮的點,往往不是 “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 + 動態模塊”最有價值的地方,就是插件系統。你通常會遇到這些痛點:

  1. 依賴衝突:插件各帶一套依賴,版本不一致
  2. 隔離需求:插件不應該隨便反射/訪問主程序內部
  3. 可卸載/可替換:插件更新不希望重啓整個系統
  4. 安全邊界:插件的權限要可控(至少在模塊級別可控)

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.javaopens ... to plugin.module 精確開放(最可審計、最穩定)
  • 運行時開門作為“動態治理手段”,不要變成默認依賴

一些“踩過坑的人才會在意”的建議

  這段我説得直一點,都是實戰裏很容易翻車的點:

  1. 插件不要隨便讀 host 模塊: host 應儘量只暴露 app.api,別讓插件“穿透到內部實現”,否則你以後重構 host 會被插件綁架。

  2. ServiceLoader 的 Layer 要明確ServiceLoader.load(Plugin.class) 默認走當前 layer/classpath,插件隔離後容易“加載不到/加載錯”。用 ServiceLoader.load(pluginLayer, Plugin.class) 更穩。

  3. 類加載器策略要可解釋: 一層一 loader 好理解也好排障;想做更復雜的共享依賴,需要更明確的依賴分層,不然排錯像抓鬼。

  4. 反射開門要最小化opens 一開,框架很爽,安全邊界就鬆了。能不開放就不開放;必須開放就“定向 opens to”。

  5. 卸載不是自動的: Layer/ClassLoader 是否能被回收,取決於你有沒有強引用(緩存了 Class、MethodHandle、單例等)。別指望“關掉插件就自動卸載”,要設計生命週期與清理策略。

小結:Layer 讓“動態加載”從技巧變成結構

  如果把模塊系統比作“城市規劃”,那 Layer 就是“分城運營”:

  • 主城(boot layer)保持穩定與邊界
  • 插件在分城(child layers)自由迭代
  • 需要交流就走明面通道(exports/uses/provides)
  • 需要特殊通行證才開小門(opens/addOpens)

最後我想用一句反問收尾(我這人嘴欠,但這句真的好用😄): 你的插件到底應該“依賴你的實現”,還是隻應該“依賴你的契約”? 如果答案是前者,那你遲早會被自己的系統鎖死;如果是後者,Layer 才真正發揮價值。

... ...

文末

好啦,以上就是我這期的全部內容,如果有任何疑問,歡迎下方留言哦,咱們下期見。

... ...

學習不分先後,知識不分多少;事無鉅細,當以虛心求教;三人行,必有我師焉!!!

wished for you successed !!!


⭐️若喜歡我,就請關注我叭。

⭐️若對您有用,就請點贊叭。 ⭐️若有疑問,就請評論留言告訴我叭。

版權聲明:本文由作者原創,轉載請註明出處,謝謝支持!