背景

Halo 目前(1.4.11)尚未有一个模块化架构,针对于扩展及生态方面的可扩展性严重不足,根据 Halo 内部第一次会议讨论后,决定在未来持续推进模块化设计进程,用于支持官方或第三方组件的快速集成。

本文档由 LIlGG 编写,目的是描述 Halo 插件开发的整体架构设计建议。整个文档中,一部分是已经验证过的设计,一部分是基于现有实际项目来的设计,还有一小部分需要整个 Halo 开发小组进行多次讨论后再进行确定。

目标

对于 Halo 将要设计的插件化,有如下几个主要目标来尝试使用本文进行解决:

  1. 对于使用者的友好设计。插件操作对于 Halo 的直接使用者应该是透明的,即使用者应当能够通过 UI 来发现和安装、更新 Halo 插件(Bundle)。
  2. 推进极致模块化。将 Halo 非核心特性拆分为 System Bundle,避免 Halo Core 过于膨胀,通过 API 或 Event 等方式对 System Bundle 与 Application 之间进行通信和联系,防止过于疏散。
  3. 建立插件生态系统。当 Halo 产生大量插件时,势必会出现插件难以发现、难以集成、兼容性的问题,建立一个插件生态系统是关键且必要的。
  4. 系统更新。Halo 的版本更新应该更为简单,最终用户采用一键更新的方式更为方便。

设计

期望

我所期望的 Halo 最终设计产品应该如下:

Halo 用户最开始只需要下载一个很小的 Halo Core,而不是越来越臃肿的完整代码包。

在第一次加载时, Halo 将在底层自动下载核心 System Bundle,例如 Admin UI。本步骤用户也可采用离线版,即核心 System Bundle 跟随 Halo Core 一同被用户下载。

之后,如同现在的启动菜单一样弹出 Admin UI 的窗口,让用户填写一些基础信息,最后再包含插件库的说明或推荐插件等。

在 Admin UI 的插件菜单中,列出可用的官方插件及一系列可供选择的社区插件。这很大程度上方便最终用户发现 Halo 生态系统。用户也可以键入第三方插件存储库来获取第三方插件或者更快的下载插件,这有助于插件存储库的灵活性。用户可以点击按钮查看阅读每个插件的详细信息并进行安装。该插件菜单甚至本身就可以作为一个插件来运行。

一旦一个插件的错误修复被发布,已安装的插件标签将会显示更新可用,用户只需要点击一下就可以进行升级。当然如果新发布的插件与现有系统版本不兼容,将会提醒用户。

当 Halo 系统需要更新时,如果仅是 System Bundle 升级,则类似于插件一样,在系统更新界面点击按钮进行升级即可,后端将自动下载相应的 Bundle。而如果 Halo Core 具有更新,则用户仅仅只需要替换最小的 Halo Core 即可。

就目前来看,上述内容还只是个期望,但本文的其余部分将会详细说明如何逐步实现上述功能。

选择插件系统

对于 Halo 插件框架,我考虑或者已经调研的有:OSGi/Felix, PF4J, Sbp, Springboot-Plugin-Framework

OSGi 作为最先关注和调研的对象,其模块化系统及其设计是目前 Java 模块化进程中优秀的一支。尤其经过多年的沉淀,现有的 OSGi Core Release 8 已经能符合各种模块化场景,并完美符合 Halo 的设计需求。但 OSGi 对于 Halo 来说却相对而言有些过于重量级,除非 Halo 需要实现完全模块化(至于目前为何不属于完全模块化后续将会提出),否则应该就简原则,使用更容易实现的方式。另外 OSGi 国内文献过少,理解和实现太困难,也由于 Spring 团队早已放弃 OSGi 的适配,因而很难实现基于 SpringBoot + OSGi 的现代企业开发体系。

Springboot-Plugin-Framework 与 Sbp 同样基于 PF4J 实现,同样的作为适配了 Springboot 的实现,两者具有很多的相似性。针对于 Springboot-Plugin-Framework 的调研和测试相对而言比较少,而对于 Sbp 已经做了不少调研和测试工作,因而选用 Sbp 是目前来说最理想的实现方式。

PF4J 是一个轻量级的 Java 插件开发框架,这里就使用它及 Sbp 的优缺点进行列举,有部分内容是针对于目前的 Halo 设计而言,并非 PF4J 或 Sbp 自身缺点。

优点:

  1. PF4J 体积小、重量轻,相较于 OSGi 来说非常灵活。
  2. 对现有代码侵入性小。
  3. 扩展方便,只需要一个注解和接口即可实现。
  4. 官方具有 Spring 的扩展开发包以及第三方的 SpringBoot 扩展开发包。
  5. 详尽的打包、更新和类加载模式,支持 Zip/Jar 包的开发部署。
  6. 具有完善的文献及社区支持。

缺点:

  1. 对于 Fat Jar 需要在部署时需要使用自定义打包方式。
  2. 插件包之间的依赖难以管理,实际生产环境中,每个插件需要具有自己的依赖,即插件尽量使用各自的类加载器来加载依赖。
  3. 部分程序必须由 Main Application 来进行加载实现,而扩展也需要由 Main API 进行扩展,无法使用类似 OSGi 的服务发现等方式来进行自由的扩展管理。
  4. 扩展 API 无法控制权限范围,针对于安全方面需要更多的代码和逻辑实现。

插件在 Halo 中的展现形式

插件(Plugins),也可以称为 扩展(Extensions)、模块(modules) 等等,但通常选用更广泛的插件作为称呼。

根据先前描述的目标和期望,Halo 中的插件将至少分为两大类。

  • System Plugins - 系统插件,由 Halo 系统开发人员负责维护的插件,用于系统更新。
  • User Plugins - 用户插件,其中用户插件也可以细分为由 Halo 官方开发的插件与社区开发的插件。

两者在格式和打包类型上并无不同,区别在于所处的插件包路径、功能划分和所拥有的 API 权限不同。(这就需要更细粒度的控制 API 接口的权限)

Halo 插件继承了 PF4J 的打包方式,其可以为 Zip 或者 Jar 包。Zip 包中包括一个包含了类路径的 “classes” 文件夹以及该插件所需的依赖 “lib” 文件夹。Jar 包则分为标准 Jar 及 Fat Jar,其中,标准 Jar 包不包含任何依赖文件,而 Fat Jar 包含了所有的依赖。

Zip or Jar?

PF4J 官方推荐使用 Zip 包进行打包,但本文认为 Jar 包对于 Java 项目来说更为合适,因此本文推荐采用 Fat Jar 包的方式进行打包。至于为何不使用标准 Jar 可以参看 附录A-标准 Jar 及 Fat Jar。但 Fat Jar 也会有其他问题,例如:

  1. Fat Jar 势必会使插件包体积变大很多
  2. 使用 Fat Jar 可能会导致系统中同时存在多个相同的依赖。(PF4J 不同的 Jar 采用不同的类加载器,因此此方面不需要太过于考虑。)
  3. Fat Jar 中会包含 Main Application 中的依赖,需要进行排除。(采用自定义打包即可解决)
  4. Fat Jar 无法像 Zip 一样存在 “classes” 及 “lib”,只能将所有依赖和类文件平铺到整个 Jar 文件内,因为默认的 Java ClassLoad 仅会直接加载 Jar 包下的文件。(可以使用自定义 Main-Class 类来重写类加载和第三方依赖包加载,参考 SpringBoot#org.springframework.boot.loader.JarLauncher,但根据目前的需求来看,不推荐实现)

一个打包后的插件 Jar 文件夹内容如下所示:

  1. halo-plugin-1.0.0.jar
  2. halo-plugin-1.0.0/
  3. ├── {classes} # class 文件
  4. ├── META-INF
  5. └── MANIFEST.MF # 插件描述符
  6. ├── application.yml # 配置文件
  7. └── {libs} # 依赖文件

其中 MANIFEST.MF 为一个插件描述符,用于介绍插件信息,其插件属性如下所示:

  1. Plugin-Id: halo-plugin // 插件编号,每个插件必须拥有唯一且固定的编号
  2. Plugin-Version: 1.0.0 // 插件版本,SemVer 表达式
  3. Plugin-Requires: >=1.4.11 // 插件所依赖系统版本。 SemVer 表达式
  4. Plugin-Class: run.halo.core.MainPlugin // 插件启动类
  5. Plugin-Dependencies: halo-plugin1, halo-plugin2@>=6.0.0 // 插件依赖关系。
  6. Plugin-Description: Halo 插件示例 Demo // 插件描述
  7. Plugin-Provider: LIlGG // 插件作者
  8. Plugin-License: GNU General Public License v3.0 // 插件许可证

插件描述符是自定义的。

Plugin-Requires属性是一个 SemVer 表达式,该属性可以告诉我们,插件作者已经使用哪个版本的 Halo 测试了插件(包括依赖 Jar)来增强兼容性,上述示例插件对于 Halo 版本大于 1.4.11 有效。

Plugin-Dependencies属性列出了需要安装哪些插件才能运行本插件。PF4J 将解析依赖图并正确启动它们,也会在缺少依赖时报出错误信息。详细信息参考 PF4J#Notes about plugin dependencies

插件所在文件夹

默认情况下,System Plugins 将位于 $HALO_HOME/systems 文件夹(可配置),而 User Plugins 将位于 $HALO_HOME/plugins 文件夹(可配置)。

每个插件都是一个 Jar 文件,可以通过手动将 Jar 文件放置在文件夹内来由 Halo 自动激活插件,这需要轮询文件夹内的文件来实现。也可以用 API 来安装插件。这取决于具体的实现。

系统插件(System Plugins)

系统插件是 Halo 独立于插件系统的自行设计,为了实现极致模块化,引入此模块。

特点

系统插件具有以下特点:

  • 作为系统的一部分,系统插件是必不可少的(也可以设计可选的系统插件),缺少必要的系统插件将导致系统无法启动。
  • 系统插件不跟随插件更新,仅当用户点击系统更新时才进行更新。
  • 用户将不允许控制系统插件的安装、卸载。

为何需要它?

实现 System Plugins 将具有以下优点

  1. 可以实现在线更新系统,告别服务器层次的操作,提升系统健壮性,减少由于系统更新导致的主题、插件不兼容而产生的问题。
  2. 对 Halo Core 进行“瘦身”,无须再下载几百M的 Jar 包。
  3. 对于小型 BUG 更新或功能更新,用户无须再重新下载下个版本的安装包来修复错误,可直接采用在线更新的方式立即修复。

安装方式

系统插件有两种安装方式

  1. 离线包安装
  2. 系统自动下载

缺点

实现系统插件的最大缺点,就是对目前现有系统需要进行重构。需要将原有的 Halo 系统修改为模块化并使用插件设计重新实现,工作量较大,但收益也较高。需要额外进行讨论

用户插件(User Plugins)

系统引导及插件加载

插件生命周期

更新存储库

插件生态

终端集成

管理界面集成

SpringBoot 相关功能

前端模块化

插件开发简易化

附录A: 名词解释

标准 Jar 及 Fat Jar

标准 Jar:仅包含类路径的 Jar 包,其中不包含任何依赖 Jar 文件,这类 Jar 包通常需要第三方 Jar 处于同一个环境或容器中才可正常运行,否则在编译或运行阶段会报错类丢失等问题。

Fat Jar:包含第三方依赖的 Jar 包,即 “All-In-One”。这类包无须任何第三方 Jar 即可直接运行,例如典型的 SpringBoot 就是采用 Fat Jar 进行打包。

Halo 插件中采用 Fat Jar 的原因是插件包不仅仅是系统开发人员进行开发维护,也会有大量社区人员参与,这类人员开发的包中势必会包含各种第三方依赖,因此采用 Fat Jar 是必要的。

附录B:概念验证

附录C:需要注意的问题

附录D:未来发展