在本文中,我想告诉您如何快速轻松地创建一个支持动态加载插件的Java应用程序框架。 读者可能会立即认为这样的任务已经解决了很长时间,您可以简单地使用现成的框架或编写您的类加载器,但是我建议的解决方案不需要这些:
- 我们不需要特殊的库或框架( OSGi ,Guice等)
- 我们不会将字节码解析与ASM和类似的库一起使用。
- 我们不会编写我们的类加载器。
- 我们将不使用反射和注释。
- 无需大惊小怪的类路径来查找插件。 我们根本不会碰到类路径。
- 另外,我们将不会使用XML,YAML或任何其他声明性语言来描述扩展点(插件中的扩展点)。
但是,仍然有一个要求-这样的解决方案仅适用于Java 9或更高版本。 因为它将基于
模块和服务 。
因此,让我们开始吧。 我们更具体地提出问题:
您需要实现一个最小的应用程序框架,该框架在启动时会从plugins
文件夹加载用户插件。
也就是说,组装后的应用程序应如下所示:
plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar …
让我们从
core
模块开始。 这个模块是我们应用程序的核心,实际上是我们的框架。
对于那些珍惜时间的人,可以在GitHub上找到完成的项目。 组装说明。友情链接 git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core
在模块中创建以下4个Java文件:
core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java
第一个文件
IService.java
是描述我们的扩展点的文件。 然后,其他插件将能够为该扩展点做出贡献(“贡献”)。 这是构建插件应用程序的标准原理,称为
依赖关系反转 (Dependency Inversion)
原理 。 该原理基于以下事实:内核不依赖于特定的类,而是依赖于接口。
我给扩展点一个抽象名称
IService
,因为我现在仅演示一个概念。 实际上,它可以是任何特定的扩展点,例如,如果您正在编写图形编辑器,则可以是图像处理的效果,例如
IEffectProvider
,
IEffectContribution
或其他,这取决于您更喜欢如何命名扩展点。 同时,应用程序本身将包含一些基本的效果集,第三方开发人员将能够编写其他更复杂的效果并以插件形式提供。 用户只需要将这些效果放入
plugins
文件夹中,然后重新启动应用程序即可。
IService.java
文件如下:
… public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } }
因此,
IService
只是做一些抽象的
doJob()
的接口(我再说一遍,细节并不重要,实际上这将是具体的)。
还要注意第二个
getServices()
方法。 此方法返回在此模块层及其父级中找到的
IService
接口的所有实现。 稍后我们将详细讨论。
第二个文件
BasicService.java
是
IService
接口的基本实现。 即使应用程序中没有插件,它也将始终存在。 换句话说,
core
不仅是核心,而且同时是一个插件,它将始终被加载。
BasicService.java
文件如下所示:
… public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } }
为简单起见,
doJob()
仅显示字符串
"Basic service"
,仅此而已。
因此,目前我们有以下图片:

第三个文件
Main.java
是
main()
方法的实现位置。 要了解哪些内容,您需要了解什么是模块层。
关于模块层
Java启动应用程序时,参数
--module-path
(以及
classpath
(如果有))中列出的所有平台模块+模块都位于所谓的
Boot
层中。 在我们的例子中,如果我们编译core.jar模块并从命令行运行
java --module-path core.jar --module core
,那么至少
java.base
和
core
模块将位于
Boot
层:

Boot
层始终存在于任何Java应用程序中,这是最小的配置。 大多数应用程序存在于模块的单个层中。 但是,在我们的情况下,我们想从
plugins
文件夹中动态加载
plugins
。 我们可以强迫用户更正应用程序启动行,以便他自己将必需的插件添加到
--module-path
,但这不是最佳解决方案。 尤其是那些不是程序员并且不了解为什么他们需要爬到某个地方并为这样简单的事情解决问题的人。
幸运的是,有一个解决方案:Java允许您在运行时创建自己的模块层,这将从我们需要的位置加载模块。 就我们的目的而言,一个新的插件层就足够了,它将有一个
Boot
层作为父层(任何一层都必须有一个父层):

插件层具有
Boot
层作为父层的事实意味着,插件层中的模块可以引用
Boot
层中的模块,反之亦然。
因此,现在知道什么是模块层,您终于可以看一下
Main.java
文件的内容:
… public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins");
如果这是您第一次查看此代码,它可能看起来很复杂,但是由于存在大量新的未知类,因此这是一种错误的感觉。 如果您对
ModuleFinder ,
Configuration和
ModuleLayer类的含义有所了解,那么一切都准备就绪。 而且,只有几十行! 这就是一次编写的所有逻辑。
模块描述符
还有一个我们没有考虑的文件(第四个):
module-info.java
。 这是最短的文件,包含我们模块的声明和服务描述(扩展点):
… module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; }
该文件各行的含义应该很明显:
- 首先,该模块导出
org.example.pluginapp.core
包, org.example.pluginapp.core
插件可以从IService
接口继承(否则,在core
模块外部将无法访问IService
)。 - 其次,他宣布他正在使用
IService
。 - 第三,他说他通过
BasicService
类提供了IService
服务的BasicService
。
由于模块声明是用Java编写的,因此我们获得了非常重要的优势:
编译器检查和静态保证 。 例如,如果我们在类型名称上输入错误或指示不存在的程序包,我们将立即得到它。 对于某些OSGi,由于扩展点的声明将以XML编写,因此在编译时我们不会进行任何检查。
因此,框架已准备就绪。 让我们尝试运行它:
> java --module-path core.jar --module core Basic service
发生什么事了
- Java尝试在
plugins
文件夹中找到模块,但未找到任何模块。 - 空层已创建。
- ServiceLoader开始搜索所有
IService
实现。 - 在空层中,他没有找到任何服务实现,因为那里没有模块。
- 在这一层之后,他继续在父层(即
Boot
层)中搜索,并在core
模块中找到BasicService
一种实现。 - 找到的所有实现都调用了
doJob()
方法。 由于仅找到一种实现,因此仅打印"Basic service"
。
编写插件
编写了应用程序的核心之后,现在该尝试为其编写插件了。 让我们编写两个插件
plugin1
和
plugin2
:让第一个打印
"Service 1"
,第二个打印
"Service 2"
。 为此,您需要分别在
plugin1
和
plugin2
提供两个
IService
实现:

用两个文件创建第一个插件:
plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java
Service1.java
文件:
… public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } }
module-info.java
文件:
… module plugin1 { requires core; provides IService with Service1; }
请注意,
plugin1
是
core
相关的。 这就是我之前提到的依赖关系反转原理:内核不依赖于插件,反之亦然。
第二个插件与第一个插件完全相似,因此在此不再赘述。
现在,让我们收集插件,将其放入
plugins
文件夹中并运行应用程序:
> java --module-path core.jar --module core Service 1 Service 2 Basic service
万岁,插件被选中! 这是怎么发生的:
- Java在
plugins
文件夹中找到了两个模块。 - 使用两个模块
plugins1
和plugins2
创建了一个层。 - ServiceLoader开始搜索所有
IService
实现。 - 在插件层,他找到了
IService
服务的两个实现。 - 之后,他继续在父层(即
Boot
层)中搜索,并在core
模块中找到BasicService
一种实现。 - 找到的所有实现都调用了
doJob()
方法。
请注意,正是因为对服务提供者的搜索从子层开始,然后到父层,所以先打印
"Service 1"
和
"Service 2"
,然后打印
"Basic Service"
。 如果您希望对服务进行排序,以便先对基本服务进行排序,然后对插件进行排序,则可以通过添加排序来调整
IService.getServices()
方法(您可能需要向
IService
接口添加
int getOrdering()
方法)。
总结
因此,我展示了如何快速有效地组织具有以下属性的插件Java应用程序:
- 简便性:对于扩展点及其绑定,仅使用基本的Java功能(接口,类和ServiceLoader ),而没有框架,反射,注释和类加载器。
- 可声明性:扩展点在模块描述符中描述。 只需查看
module-info.java
并了解存在哪些扩展点以及哪些插件有助于这些点。 - 静态保证:如果模块描述符中的错误,程序将无法编译。 另外,作为奖励,如果您使用IntelliJ IDEA,则会收到其他警告(例如,如果您忘记
uses
并使用ServiceLoader.load()
)。 - 安全性:模块化Java系统在启动时检查模块的配置是否正确,并在出现错误的情况下拒绝执行程序。
我再说一次,我只展示了这个主意。 在实际的插件应用程序中,将有数十至数百个模块和数百至数千个扩展点。
我之所以决定提出这个主题,是因为在过去的7年中,我一直在使用Eclipse RCP编写模块化应用程序,其中臭名昭著的OSGi用作插件系统,并且插件描述符用XML编写。 我们有一百多个插件,我们仍然使用Java8。但是,即使我们切换到新版本的Java,我们也不大可能使用Java模块,因为它们与OSGi紧密相关。
但是,如果您是从头开始编写插件应用程序,那么Java模块是其实现的可能选择之一。 请记住,模块只是工具,而不是目标。
简要介绍一下我
我已经编程了10多年(其中有8年使用Java),我对
StackOverflow做出了回应,并
在Telegram中运行了自己的专门用于Java的
频道 。