JExten:基于Java模块系统(JPMS)构建健壮的插件架构
在Java中构建可扩展应用程序时,开发者常常从一个简单的问题开始:"如何让用户无需重新编译核心应用程序就能添加功能?" 旅程通常始于标准的 然而,随着应用程序的增长,一个关键问题出现了:"类路径地狱"。 想象一下,你有一个使用 这正是JExten背后的核心驱动力。我需要一种能够严格封装插件的方式,使得每个插件都可以定义自己的依赖,而不影响主机或其他插件。 Java 9引入了模块系统(JPMS),它提供了强封装和显式的依赖关系图。它允许我们创建隔离的模块"层"。 通过利用JPMS的 JExten设计为轻量级且基于注解驱动,抽象了原始 架构基于三个主要支柱: 核心在于,JExten在"契约"(API)和"实现"之间进行了清晰分离。 扩展点 ( 扩展 ( 注意,你可以在没有 为了分离关注点,该库将职责划分到两个不同的管理器: PluginManager("物理层"): ExtensionManager("逻辑层"): 由于插件在隔离的层中运行,标准的DI框架(如Spring或Guice)有时可能"太重"或在动态模块边界间配置起来很棘手。JExten包含一个内置的、轻量级的DI系统。 你可以简单地使用 这在模块边界之间可以无缝工作。一个插件可以注入由主机提供的服务,甚至可以注入由另一个插件提供的服务(如果模块关系图允许的话)。 下面快速了解一下如何定义扩展点,在插件中实现它,并在应用程序中使用。 创建一个接口并用 在你的插件模块中,实现该接口并用 在你的主机应用程序中,使用 最后,使用 然后,你可以将生成的ZIP包安装到你的主机应用程序中: 选择合适的插件框架取决于你的具体需求。以下是JExten与一些成熟替代方案的对比: PF4J是一个成熟的、轻量级的插件框架,依赖于ClassLoader隔离。 OSGi是模块化的黄金标准,为Eclipse等IDE提供支持。 Layrry是一个用于执行模块化Java应用程序的启动器和API。 JExten是一个轻量级、基于注解驱动的插件框架,它利用JPMS模块层来提供隔离和依赖管理。其设计目标是易于使用和理解,注重简洁性和易用性。 最后,请记住JExten仍处于早期阶段,有很大的改进空间。欢迎在GitHub上为项目做贡献和/或在issue部分参与讨论。项目仓库链接在此处。 【注】本文译自:JExten: Building a Robust Plugin Architecture with Java Modules (JPMS) - DEV CommunityJExten:基于Java模块系统(JPMS)构建健壮的插件架构
1. 动机:通往模块化隔离之路
java.util.ServiceLoader,它提供了一种发现接口实现的简单机制。library-v1 的主机应用程序。你创建了一个插件系统,有人写了一个需要 library-v2 的 "Twitter 插件"。如果所有东西都在同一个扁平的类路径上运行,就会产生冲突。要么主机因为得到错误的库版本而崩溃,要么插件失败。你无法在类路径上同时存在同一个库的两个版本而不面临运行时异常(如 ClassDefNotFoundError 或 NoSuchMethodError)的风险。
引入JPMS(Java平台模块系统)
ModuleLayers,JExten允许插件A依赖于Jackson 2.14,而插件B依赖于Jackson 2.10,两者可以在同一个运行的应用程序中和睦共存。2. 架构与设计
ModuleLayers的复杂性,同时提供了依赖注入(DI)和生命周期管理等强大功能。扩展模型
@ExtensionPoint):在主机应用程序(或共享API模块)中定义的接口,规定了哪些功能可以被扩展。@ExtensionPoint(version = "1.0")
public interface PaymentGateway {
void process(double amount);
}@Extension):由插件提供的具体实现。@Extension(priority = Priority.HIGH)
public class StripeGateway implements PaymentGateway {
// ...
}PluginManager的情况下使用ExtensionManager。这在测试中或当你希望在非插件环境中使用JExten,且所有扩展都已经在模块路径中可用时非常有用。管理器分离
ModuleLayer 图。它读取 plugin.yaml 清单,解析依赖项(从本地缓存或Maven仓库),并构建类加载环境。@Extension 注解的类。依赖注入
@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>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. 与其他解决方案的比较
PF4J (Plugin Framework for Java)
module-info.java)来定义依赖关系,而不是自定义清单。OSGi
Layrry
特性 JExten PF4J OSGi Layrry 隔离 JPMS 模块层 文件/类加载器 Bundle类加载器 JPMS 模块层 配置 Java 注解 属性/清单 Manifest 头部 YAML/TOML 依赖注入 内置 ( @Inject)外部 (Spring/Guice) 声明式服务 无 (ServiceLoader) 学习曲线 低 低 高 中 5. 结论