JExten:基于Java模块系统(JPMS)构建健壮的插件架构

91 阅读7分钟

JExten:基于Java模块系统(JPMS)构建健壮的插件架构

1. 动机:通往模块化隔离之路

在Java中构建可扩展应用程序时,开发者常常从一个简单的问题开始:"如何让用户无需重新编译核心应用程序就能添加功能?" 旅程通常始于标准的 java.util.ServiceLoader,它提供了一种发现接口实现的简单机制。

然而,随着应用程序的增长,一个关键问题出现了:"类路径地狱"。

想象一下,你有一个使用 library-v1 的主机应用程序。你创建了一个插件系统,有人写了一个需要 library-v2 的 "Twitter 插件"。如果所有东西都在同一个扁平的类路径上运行,就会产生冲突。要么主机因为得到错误的库版本而崩溃,要么插件失败。你无法在类路径上同时存在同一个库的两个版本而不面临运行时异常(如 ClassDefNotFoundErrorNoSuchMethodError)的风险。

这正是JExten背后的核心驱动力。我需要一种能够严格封装插件的方式,使得每个插件都可以定义自己的依赖,而不影响主机或其他插件。

引入JPMS(Java平台模块系统)

Java 9引入了模块系统(JPMS),它提供了强封装和显式的依赖关系图。它允许我们创建隔离的模块"层"。

  • 启动层:JVM和平台模块。

  • 主机层:核心应用程序及其依赖。

  • 插件层:在主机层之上动态创建的层。

通过利用JPMS的ModuleLayers,JExten允许插件A依赖于Jackson 2.14,而插件B依赖于Jackson 2.10,两者可以在同一个运行的应用程序中和睦共存。

2. 架构与设计

JExten设计为轻量级且基于注解驱动,抽象了原始ModuleLayers的复杂性,同时提供了依赖注入(DI)和生命周期管理等强大功能。

架构基于三个主要支柱:

扩展模型

核心在于,JExten在"契约"(API)和"实现"之间进行了清晰分离。

    1. 扩展点 (@ExtensionPoint):在主机应用程序(或共享API模块)中定义的接口,规定了哪些功能可以被扩展。

@ExtensionPoint(version = "1.0")

public interface PaymentGateway {

void process(double amount);

}

    1. 扩展 (@Extension):由插件提供的具体实现。

@Extension(priority = Priority.HIGH)

public class StripeGateway implements PaymentGateway {

// ...

}

注意,你可以在没有PluginManager的情况下使用ExtensionManager。这在测试中或当你希望在非插件环境中使用JExten,且所有扩展都已经在模块路径中可用时非常有用。

管理器分离

为了分离关注点,该库将职责划分到两个不同的管理器:

  1. PluginManager("物理层")
  • 该组件处理原始工件(JAR/ZIP文件)。

  • 它使用SHA-256校验和验证完整性,确保插件未被篡改。

  • 它构建JPMS ModuleLayer 图。它读取 plugin.yaml 清单,解析依赖项(从本地缓存或Maven仓库),并构建类加载环境。

  1. ExtensionManager("逻辑层")
  • 一旦层构建完成,该组件接管。

  • 它在各个层中扫描带有 @Extension 注解的类。

  • 它管理这些扩展的生命周期(单例、会话或原型作用域)。

  • 它处理依赖注入。

依赖注入

由于插件在隔离的层中运行,标准的DI框架(如Spring或Guice)有时可能"太重"或在动态模块边界间配置起来很棘手。JExten包含一个内置的、轻量级的DI系统。

你可以简单地使用 @Inject 将扩展连接在一起:


@Extension

public class MyPluginService {

@Inject

private PaymentGateway gateway; // 自动注入最高优先级的实现

}

这在模块边界之间可以无缝工作。一个插件可以注入由主机提供的服务,甚至可以注入由另一个插件提供的服务(如果模块关系图允许的话)。

3. 使用示例

下面快速了解一下如何定义扩展点,在插件中实现它,并在应用程序中使用。

I. 定义一个扩展点

创建一个接口并用 @ExtensionPoint 注解。这是插件将实现的契约。


@ExtensionPoint(version = "1.0")

public interface Greeter {

void greet(String name);

}

II. 实现一个扩展

在你的插件模块中,实现该接口并用 @Extension 注解。


@Extension

public class FriendlyGreeter implements Greeter {

@Override

public void greet(String name) {

System.out.println("Hello, " + name + "!");

}

}

III. 发现与使用

在你的主机应用程序中,使用 ExtensionManager 来发现和调用扩展。


public class Main {

public static void main(String[] args) {

// 初始化管理器

ExtensionManager manager = ExtensionManager.create(pluginManager);

  


// 获取Greeter扩展点的所有扩展

manager.getExtensions(Greeter.class)

.forEach(greeter -> greeter.greet("World"));

}

}

IV. 将你的扩展打包为插件

最后,使用 jexten-maven-plugin Maven插件在编译时检查你的 module-info.java,并将你的扩展打包成一个包含所有依赖项和生成的 plugin.yaml 清单的ZIP包。


<plugin>

<groupId>org.myjtools.jexten</groupId>

<artifactId>jexten-maven-plugin</artifactId>

<version>1.0.0</version>

<executions>

<execution>

<goals>

<goal>generate-manifest</goal>

<goal>assemble-bundle</goal>

</goals>

</execution>

</executions>

<configuration>

<hostModule>com.example.app</hostModule>

</configuration>

</plugin>

然后,你可以将生成的ZIP包安装到你的主机应用程序中:


public class Application {

public static void main(String[] args) throws IOException {

Path pluginDir = Path.of("plugins");

  


// 创建插件管理器

PluginManager pluginManager = new PluginManager(

"org.myjtools.jexten.example.app", // 应用程序ID

Application.class.getClassLoader(),

pluginDir

);

  


// 从ZIP包安装插件

pluginManager.installPluginFromBundle(

pluginDir.resolve("my-plugin-1.0.0.zip")

);

  


// 创建支持插件的扩展管理器

ExtensionManager extensionManager = ExtensionManager.create(pluginManager);

  


// 从插件获取扩展

extensionManager.getExtensions(Greeter.class)

.forEach(greeter -> greeter.greet("World"));

}

}

4. 与其他解决方案的比较

选择合适的插件框架取决于你的具体需求。以下是JExten与一些成熟替代方案的对比:

PF4J (Plugin Framework for Java)

PF4J是一个成熟的、轻量级的插件框架,依赖于ClassLoader隔离。

  • 隔离性:PF4J使用自定义ClassLoader隔离插件。JExten使用JPMS ModuleLayers。后者是自Java 9以来处理隔离的"原生"Java方式,在JVM级别严格执行封装。

  • 现代性:虽然PF4J非常优秀,但JExten是专门为现代模块化Java生态系统(Java 21+)设计的,利用模块描述符(module-info.java)来定义依赖关系,而不是自定义清单。

OSGi

OSGi是模块化的黄金标准,为Eclipse等IDE提供支持。

  • 复杂性:OSGi功能强大,但学习曲线陡峭且样板代码多(Manifest头、Activators、复杂的服务动态)。JExten通过专注于80%的用例——具有简单依赖注入的严格隔离扩展——提供了远低于OSGi的复杂性("精简版OSGi"),且不需要完整的OSGi容器。

  • 运行时:OSGi带来沉重的运行时。JExten是一个轻量级库,构建在标准JVM特性之上。

Layrry

Layrry是一个用于执行模块化Java应用程序的启动器和API。

  • 关注点:Layrry非常侧重于模块层的配置和组装(通常通过YAML/TOML),并充当运行器。JExten则侧重于这些层内的编程模型

  • 特性:Layrry擅长构建层,但它不提供固化的应用框架。JExten提供了"粘合"代码——扩展点、依赖注入和生命周期管理——这些是你在使用原始模块层或Layrry时必须自己编写的东西。

| 特性 | JExten | PF4J | OSGi | Layrry |

| :--- | :--- | :--- | :--- | :--- |

| 隔离 | JPMS 模块层 | 文件/类加载器 | Bundle类加载器 | JPMS 模块层 |

| 配置 | Java 注解 | 属性/清单 | Manifest 头部 | YAML/TOML |

| 依赖注入 | 内置 (@Inject) | 外部 (Spring/Guice) | 声明式服务 | 无 (ServiceLoader) |

| 学习曲线 | 低 | 低 | 高 | 中 |

5. 结论

JExten是一个轻量级、基于注解驱动的插件框架,它利用JPMS模块层来提供隔离和依赖管理。其设计目标是易于使用和理解,注重简洁性和易用性。

最后,请记住JExten仍处于早期阶段,有很大的改进空间。欢迎在GitHub上为项目做贡献和/或在issue部分参与讨论。项目仓库链接在此处


【注】本文译自:JExten: Building a Robust Plugin Architecture with Java Modules (JPMS) - DEV Community