跳到主要内容

插件(Plugin)

插件模块采用独立的类加载器(ClassLoader)来管理私有包、类、资源文件等,设计目标是在接口开发模式下,将需求进行更细颗粒度拆分,从而达到一个理想化可重用代码的封装形态。

每个插件都是封闭的世界,插件与外界之间沟通的唯一方法是通过业务接口调用,管理这些插件的容器被称之为插件工厂(IPluginFactory),其负责插件的分析、加载和初始化,以及插件的生命周期管理,插件模块支持创建多个插件工厂实例,工厂对象之间完全独立,无任何依赖关系。

Maven包依赖​

<dependency>
<groupId>net.ymate.platform</groupId>
<artifactId>ymate-platform-plugin</artifactId>
<version>2.1.4</version>
</dependency>

插件模块的整体结构​

IApplication (YMP 容器)
└── Plugins (模块, IPlugins) ── Plugins.get()
└── IPluginFactory (插件工厂)
├── IPluginConfig 配置
├── IBeanLoader 插件专属对象加载器
├── IPluginBeanFactory 插件Bean容器(父容器 = 宿主 BeanFactory)
│ └── PluginMeta ... 插件元数据注册表
└── PluginClassLoader ... 每个外部插件目录一个独立类加载器
└── IPlugin 实例 ← @Plugin 声明的插件启动类(同时实现业务接口)

相关接口职责说明:

接口/类说明
IPlugins / Plugins插件模块(YMP Module),Plugins.get() 返回默认模块实例,持有默认插件工厂
IPluginFactory插件工厂:负责插件的加载、注册、启动、停止、卸载与重载
IPluginConfig插件模块配置,定义了配置项的常量名称
IPluginBeanFactory插件 Bean 容器,以宿主 BeanFactory 作为父容器,注册表存放 PluginMeta
IPlugin插件实例契约(初始化、启动、停止、销毁)
AbstractPluginIPlugin 的抽象实现,自定义插件建议直接继承
IPluginContext插件环境上下文,可获取所属容器、工厂与自身元数据
PluginMeta插件元数据描述对象(由 @Plugin 注解解析而来)
PluginClassLoader插件类加载器,承载插件私有的 JAR 包与类文件

默认插件工厂​

插件工厂分为两种,一种是以模块的形式封装,由框架初始化时根据配置参数自动构建,称之为默认插件工厂(有且仅能存在一个默认工厂实例),另一种是通过代码手动配置构建的自定义插件工厂,不同之处在于默认插件工厂与框架结合得更紧密,两种模式可以并存。

默认插件工厂的初始化时序如下:

  1. Plugins.onBeforeInit:读取模块配置(properties / 编程式),结合主类上的 @PluginConf 注解,构建默认插件工厂
  2. Plugins.doLoadConfig:将插件配置对象挂载到模块
  3. Plugins.onInit:注册 PluginEvent 事件,并监听框架的 APPLICATION_INITIALIZED 事件,随后调用 pluginFactory.initialize(owner) 完成插件的加载与注册
  4. 容器初始化完成后触发 APPLICATION_INITIALIZED,此时调用 pluginFactory.startup() 逐个初始化并启动插件
  5. Plugins.onClose:调用 pluginFactory.close(),逐个停止并销毁插件

注意:插件是在容器初始化完成之后才被启动的,因此不要在插件的 doInitialize 中假定其它业务模块已经完全就绪。

配置文件参数说明​

#-------------------------------------
# Plugin插件模块初始化参数
#-------------------------------------

# 是否启用插件模块(若禁用则在框架初始化不会创建默认插件工厂对象,不影响自定义插件工厂的使用),默认为true
ymp.configs.plugin.enabled=

# 插件主目录路径,可选参数,默认值为${root}/plugins(路径不存在时将自动创建)
ymp.configs.plugin.plugin_home=

# 插件自动扫描的包名前缀集合(若未设置将默认包含主程序类所在包),多个包名之间用'|'分隔
ymp.configs.plugin.package_names=

# 插件自动扫描时排除包名称集合,多个包名之间用'|'分隔, 被包含在包路径下的类文件在扫描过程中将被忽略
ymp.configs.plugin.excluded_package_names=

# 插件自动扫描时排除包文件名称集合,多个包名之间用'|'分隔, 被包含的JAR或ZIP文件在扫描过程中将被忽略
ymp.configs.plugin.excluded_file_names=

# 是否允许插件自动启动,默认为true
ymp.configs.plugin.automatic=

# 是否加载当前CLASSPATH内的所有包含插件配置文件的JAR包,默认为false
ymp.configs.plugin.included_classpath=

# 设置指定插件插件状态为禁用(xxx代表插件唯一标识或插件类名称,其值必须是disabled)
ymp.params.plugin.xxx=disabled

编程式配置(DefaultPluginConfigurable)​

除配置文件外,也可以通过 DefaultPluginConfigurable 以编码方式向传入:

IModuleConfigurer moduleConfigurer = DefaultPluginConfigurable.builder()
.enabled(true)
.pluginHome("${root}/plugins")
.packageNames("com.company|cn.company")
.automatic(true)
.includedClasspath(false)
.build();

说明:DefaultPluginConfigurable.Builder 提供 enabled / pluginHome / packageNames / excludedPackageNames / excludedFileNames / automatic / includedClasspath 七个配置项,与配置文件参数及 @PluginConf 注解一一对应(excludedPackageNames 与 excludedFileNames 于 2.1.4 起提供)。

配置注解参数说明​

@PluginConf​

配置注解声明在主程序类(即 IApplication.SYSTEM_MAIN_CLASS 指定的类)上,其作用是作为上述配置文件参数的兜底默认值——当配置文件中未读取到对应配置项时,才会采用注解值。

配置项描述
enabled是否启用插件模块,默认为 true
pluginHome插件主目录路径,可选参数,默认值为 ${root}/plugins
packageNames插件自动扫描的包名前缀集合(若未设置将默认包含主程序类所在包),可选参数
excludedPackageNames插件自动扫描时排除包名称集合,可选参数
excludedFileNames插件自动扫描时排除包文件名称集合,可选参数
includeClasspath是否扫描当前 CLASSPATH 内的相关插件,默认为 false
automatic是否允许插件自动启动,默认为 true

示例代码:

@EnableAutoScan
@PluginConf(enabled = true,
pluginHome = "${root}/plugins",
packageNames = {"com.company", "cn.company"},
automatic = true)
public class Starter {

static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}

public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
// ...
}
}
}

默认插件工厂事件​

默认插件工厂是通过框架的事件服务订阅进行处理,包括以下事件类型:

事务类型说明
PLUGIN_INITIALIZED插件初始化事件
PLUGIN_STARTED插件启动事件
PLUGIN_SHUTDOWN插件停止事件
PLUGIN_DESTROYED插件销毁事件

监听示例:

Events events = application.getEvents();
events.registerListener(Events.MODE.NORMAL, PluginEvent.class, (IEventListener<PluginEvent>) context -> {
// 事件源对象即为对应的插件实例
IPlugin plugin = context.getOwner();
System.out.println(context.getEventName() + " -> " + plugin.getPluginContext().getPluginMeta().getId());
return false;
});

注意:上述事件仅由默认插件工厂(即插件模块)触发。这是因为事件的投递是由插件模块在创建默认工厂时安装的一层 IPluginEventListener 桥接器完成的,通过代码手工创建的自定义插件工厂不会触发该事件(详见"自定义插件工厂事件处理"一节)。

自定义插件工厂​

创建自定义插件工厂时,需要对该插件工厂进行一系列配置,框架针对插件工厂的配置提供了注解和手工编码两种方式,可以根据实际情况自由选择,在通常情况下,基于注解的配置方式更为适用。

方式一:基于注解配置​

@PluginFactory​

配置项描述
pluginHome插件存放路径,必需提供
packageNames自动扫描包名,默认为插件工厂所在包路径
automatic插件是否自动启动,默认为 true
listenerClass插件生命周期事件监听器类对象,可选配置,若未提供则采用系统默认实现
loaderFactoryClass插件对象加载器工厂类,可选配置,若未提供则采用系统默认实现

示例代码:​

@PluginFactory(pluginHome = "${root}/plugins",
packageNames = {"com.company", "cn.company"},
automatic = true,
listenerClass = DemoPluginEventListener.class)
public class DemoPluginFactory extends DefaultPluginFactory {

public DemoPluginFactory(IPluginConfig pluginConfig) {
super(pluginConfig, false);
}
}

@EnableAutoScan
public class Starter {

static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}

public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
IPluginFactory pluginFactory = DefaultPluginFactory.create(application, DemoPluginFactory.class);
pluginFactory.getPluginMetas()
.forEach(pluginMeta -> System.out.println(pluginMeta.getName()));
}
}
}

注意:

  • 自定义插件工厂的构造签名必须为 (IPluginConfig);上述代码中没有显式指定配置对象,框架将依据 @PluginFactory 注解自动解析配置,若类上未声明该注解且未传入配置对象,则工厂可能创建失败。
  • @PluginFactory.pluginHome 的值会直接用于构建路径,不会自动进行环境变量替换,因此需要自行调用 RuntimeUtils.replaceEnvVariable 处理后再传入。

方式二:手工编码配置​

示例代码:​

public class DemoPluginFactory extends DefaultPluginFactory {

public DemoPluginFactory(IPluginConfig pluginConfig) {
super(pluginConfig, false);
}
}

@EnableAutoScan
public class Starter {

static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}

public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
IPluginConfig pluginConfig = DefaultPluginConfig.builder()
.pluginHome(new File(RuntimeUtils.replaceEnvVariable("${root}/plugins")))
.packageNames(Arrays.asList("com.company", "cn.company"))
.automatic(true)
.eventListener(new DemoPluginEventListener())
.build();
IPluginFactory pluginFactory = DefaultPluginFactory.create(application, DemoPluginFactory.class, pluginConfig);
pluginFactory.getPluginMetas()
.forEach(pluginMeta -> System.out.println(pluginMeta.getName()));
}
}
}

DefaultPluginConfig.Builder 参数说明​

方法描述
pluginHome插件主目录路径(File 类型)
packageNames自动扫描包名集合(List<String>),追加且去重
excludedPackageNames排除的包名集合
excludedFileNames排除的 JAR 或 ZIP 文件名称集合
automatic是否自动启动插件,手工构建时默认为 false,需要自动启动必须显式设置为 true
enabled是否启用该插件工厂,默认为 true
eventListener设置插件生命周期事件监听器,未提供时采用系统默认实现(空实现)
beanLoaderFactory设置插件对象加载器工厂,未提供时采用系统默认实现

除上述两种方式外,若无需自定义工厂类型,也可以直接采用默认的插件工厂实现:

IPluginFactory pluginFactory = DefaultPluginFactory.create(application,
RuntimeUtils.replaceEnvVariable("${root}/plugins"),
new String[]{"com.company", "cn.company"});

注意:该重载内部直接使用传入的字符串构建路径,不会像 plugin_home 配置项那样自动进行环境变量替换,因此需要自行调用 RuntimeUtils.replaceEnvVariable 处理。

自定义插件工厂事件处理​

自定义插件工厂的事件处理方式与默认插件工厂有所不同,须通过 IPluginEventListener 接口实现插件生命周期事件监听,而默认插件工厂也是通过此接口与框架事件服务集成。在前面的示例代码中,用到了一个名称为 DemoPluginEventListener 的类,此类就是通过实现该接口进行事件处理的,接口方法及说明如下:

事件说明
onInitialized插件初始化完毕将调用此方法
onStarted插件启动完毕后将调用此方法
onShutdown插件停止完毕将调用此方法
onDestroy插件初销毁前将调用此方法

示例代码:​

public class DefaultPluginEventListener implements IPluginEventListener {

@Override
public void onInitialized(IPluginContext context, IPlugin plugin) {
System.out.println("onInitialized: " + context.getPluginMeta().getName());
}

@Override
public void onStarted(IPluginContext context, IPlugin plugin) {
System.out.println("onStarted: " + context.getPluginMeta().getName());
}

@Override
public void onShutdown(IPluginContext context, IPlugin plugin) {
System.out.println("onShutdown: " + context.getPluginMeta().getName());
}

@Override
public void onDestroy(IPluginContext context, IPlugin plugin) {
System.out.println("onDestroy: " + context.getPluginMeta().getName());
}
}

框架内置的 DefaultPluginEventListener 是一个空实现(四个方法体均为空),其作用仅作为缺省占位;若 listenerClass 指定的类无法实例化,则回退到该空实现,此时所有回调都不会产生任何效果。

各回调方法在插件生命周期中的触发时机如下:

场景触发的回调
插件初始化完成onInitialized
插件启动完成onStarted
插件被取用时的补启动onStarted
插件工厂关闭 / 卸载onShutdown → onDestroy

主目录及插件结构​

插件有两种形式,一种是将插件以JAR包文件形式存储,这类插件可以直接与工程类路径下其它依赖包一起使用,另一种是将插件类文件及插件依赖包等资源放在插件目录结构下,这类插件可以放在工程路径以外,可以多模块共用插件。

每个插件工厂所指定的 PLUGIN_HOME 根路径下都可以通过一个名称为 .plugin 的目录将一些JAR包或类等资源文件进行全局共享。每个插件都是一个独立的目录,一般以插件唯一标识命名(但不限于),并将插件相关的 JAR 包和类文件等资源分别放置在对应的 lib、classes 或其它目录下,完整的目录结构如下所示:

<PLUGIN_HOME>\
|--.plugin\
| |--lib\
| | |--xxxx.jar
| | |--...
| |--classes\
| | |--...
| |--...
|--<plugin_xxx>\
| |--lib\
| | |--xxxx.jar
| | |--...
| |--classes\
| | |--...
| |--...
|--<plugin_xxxx>\
|--...

上述目录所对应的类加载器模型如下:

  • <PLUGIN_HOME>/.plugin/:以 . 开头的目录不会被当作插件处理,其下收集到的 JAR 包与 classes 目录构成一个共享库类加载器,作为所有插件类加载器的父加载器,用于放置插件与宿主、或其它插件之间共用的依赖包,避免被重复加载
  • <PLUGIN_HOME>/<插件目录>/:每个插件目录(名称不以 . 开头)生成一个独立的类加载器,仅加载自身的 lib/*.jar 与 classes/ 目录,从而实现插件之间的类隔离
  • 若未配置 plugin_home,或该路径不存在、不是一个目录,则不会加载任何外部插件

注意:请勿将插件对外暴露的业务接口(如后文中的 IEchoService)放置在插件私有的 lib 或 classes 中,否则该接口将被插件类加载器单独加载,与宿主中的同名接口不属于同一个 Class,调用时将抛出 ClassCastException。正确的做法是将其放置于宿主工程的类路径下,或放置在 .plugin 共享库中。

类路径(CLASSPATH)内的插件​

除上述目录形态的外部插件外,插件模块还支持加载当前 CLASSPATH 内的插件,需将 included_classpath 参数(或 @PluginConf.includeClasspath)设置为 true,此时将使用应用类加载器扫描并注册插件类,同时会追加扫描框架的基础包名 net.ymate.platform。

两种插件来源的差异对比如下:

对比项pluginHome 外部插件CLASSPATH 内插件(included_classpath=true)
加载来源<PLUGIN_HOME> 下的插件目录当前 CLASSPATH 内的 JAR 包或类目录
类加载器每个插件目录独立的 PluginClassLoader与宿主共用应用类加载器
类隔离支持不支持
依赖共享通过 .plugin 目录共享天然与宿主共享
卸载 / 热重载支持不支持

两条加载路径是相互独立的,可以同时生效:先加载 CLASSPATH 内的插件,再加载 plugin_home 下的外部插件。

插件​

通过在一个实现了 IPlugin 接口的类上声明 @Plugin 注解来创建插件启动类,其将被插件工厂加载和管理,一个插件包可以包括多个插件启动类,每个插件启动类可以实现自己的业务接口对外提供的服务。

@Plugin注解参数说明​

配置项描述
id插件唯一标识,若未填写则使用初始化类名称,默认为空
name插件名称,默认为空
alias插件别名集合,默认为空
author插件作者,默认为空
email联系邮箱,默认为空
version插件版本,默认为 1.0.0
automatic是否加载后自动启动运行,默认为 true
description插件描述,默认为空

插件的 id 与 alias 均可用于按名称查找插件(即 getPlugin(String) 与 @PluginRefer("xxx")),因此需确保其在同一插件工厂内的唯一性。

IPlugin接口方法说明​

方法名称描述
initialize执行插件初始化
isInitialized判断插件是否已初始化
getPluginContext获取插件环境上下文对象
isStarted判断插件是否已启动
startup启动插件
shutdown停止插件
close销毁插件

创建自定义插件​

通过继承框架中提供的 AbstractPlugin 抽象类来构建自定义插件,该类是 IPlugin 接口的抽象实现,建议直接继承。

本例中定义了一个唯一标识为 echo_plugin 的插件并实现了其自定义的业务接口 IEchoService,需要注意的是同一个插件可以实现多个业务接口,若多个插件实现同一个业务接口,根据插件加载顺序,最后加载的插件实例对象将替换前者。

示例代码:

public interface IEchoService {

void sayHi();
}

@Plugin(id = "echo_plugin",
name = "DemoPlugin",
alias = {"echo", "demo-echo"},
author = "有理想的鱼",
email = "suninformaiton#163.com", version = "1.0.0")
public class EchoPlugin extends AbstractPlugin implements IEchoService {

@Override
protected void doInitialize(IPluginContext context) throws Exception {
System.out.println("initialized.");
}

@Override
protected void doStartup() throws Exception {
System.out.println("started.");
}

@Override
protected void doShutdown() throws Exception {
System.out.println("shutdown.");
}

@Override
public void sayHi() {
System.out.println("Hi, from Plugin.");
}
}

插件生命周期​

AbstractPlugin 将 IPlugin 的各个方法转换为 doXxx 形式的模板方法,自定义插件只需覆写对应的钩子方法即可,其状态流转规则如下:

方法前置状态说明
doInitialize未完成初始化插件工厂加载并注册完毕后被调用,此时可通过上下文对象获取插件自身的元数据(ID、别名、版本等),完成后插件进入"已初始化"状态
doStartup已完成初始化当插件工厂全局 automatic 与插件 @Plugin.automatic 均为 true 时,在容器初始化完成后自动被调用;否则将在插件首次被获取(getPlugin)或被注入(@PluginRefer)时补执行一次
doShutdown已启动插件工厂关闭或插件被卸载时被调用,同一插件实例的钩子不会被重复执行
close已完成初始化销毁插件实例,其内部会先执行一次停止动作,随后清空插件上下文对象

说明:AbstractPlugin 未提供独立的"销毁"钩子方法,应将资源释放逻辑放置在 doShutdown 中;该方法在插件停止与插件销毁两个场景下共用,需注意其幂等性。

插件上下文对象(IPluginContext)​

生命周期钩子与业务方法中均可以通过 getPluginContext() 方法获取插件上下文对象,其继承自框架的 IContext 接口,因此可以访问所属容器(getOwner())等环境对象,同时提供了插件相关的两个方法:

方法名称描述
getPluginMeta获取插件元数据描述对象
getPluginFactory获取管理当前插件的插件工厂

插件元数据(PluginMeta)​

PluginMeta 由 @Plugin 注解解析而来,其主要属性如下:

属性名称描述
id插件唯一标识
name插件名称
alias插件别名集合
initClass插件初始化类(即插件启动类)
version插件版本
path插件文件存放路径(外部插件为其所在的插件目录)
author / email插件作者与联系邮箱
classLoader当前插件的类加载器
extensionObject插件扩展对象(详见"插件扩展"一节)
automatic加载时是否自动启动
disabled是否已禁用
description插件描述

获取插件元数据的途径有两种,一是通过插件上下文对象获取自身元数据,二是通过插件工厂查询:

// 方式一:插件内部
PluginMeta meta = getPluginContext().getPluginMeta();
// 方式二:插件工厂侧(支持按 ID 或别名查询)
PluginMeta meta = Plugins.get().getPluginFactory().getPluginMeta("echo_plugin");

插件的使用​

下面的示例代码展示了如何获取自定义的插件实例并调用其业务接口方法:

@EnableAutoScan
public class Starter {

static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}

public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
// 方式一:获取插件实例对象
EchoPlugin echoPlugin = Plugins.get().getPlugin(EchoPlugin.class);
// echoPlugin = (EchoPlugin) Plugins.get().getPlugin("echo_plugin");
echoPlugin.sayHi();
// 方式二:直接获取业务接口实例对象
IEchoService echoService = Plugins.get().getPlugin(IEchoService.class);
echoService.sayHi();
}
}
}

若插件设置了 automatic = false,上述获取动作同样会触发插件的启动;若插件尚未完成初始化或已被禁用、卸载,则将返回 null。

通过依赖注入引用插件​

通过 @PluginRefer 注解指定注入插件实例,注解参数说明如下:

配置项描述
value插件唯一标识或别名,若不指定则默认根据成员对象类型查找插件

示例代码:

@Bean
public class Demo {

@PluginRefer
private IEchoService echoService;

public IEchoService getEchoService() {
return echoService;
}
}

@EnableAutoScan
public class Starter {

static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}

public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
Demo demo = application.getBeanFactory().getBean(Demo.class);
demo.getEchoService().sayHi();
}
}
}

注意:@PluginRefer 的注入动作由插件工厂注册到宿主容器的注入器完成,因此被注入的成员对象必须是宿主 BeanFactory 管理的 Bean;同时插件是在容器初始化完成之后才被启动的,故不要在构造器中使用被注入的插件对象,可改在 IBeanInitializer 的 afterInitialized 方法中使用。

插件扩展(IPluginExtension)​

在某些场景下,宿主希望以一种统一的方式从插件中取得一个业务对象,而不必关心插件自身的类型,这时可以让插件启动类同时实现 IPluginExtension 接口:

public interface IPluginExtension<T> {

T getExtensionObject(IPluginContext context);
}

示例代码:

@Plugin(id = "echo_plugin")
public class EchoPlugin extends AbstractPlugin implements IPluginExtension<IEchoService> {

@Override
protected void doInitialize(IPluginContext context) throws Exception {
}

@Override
protected void doStartup() throws Exception {
}

@Override
protected void doShutdown() throws Exception {
}

@Override
public IEchoService getExtensionObject(IPluginContext context) {
return new EchoServiceImpl();
}
}

宿主侧通过插件元数据获取扩展对象,从而无需依赖插件的任何类型:

IEchoService echoService = (IEchoService) Plugins.get()
.getPluginFactory()
.getPluginMeta("echo_plugin")
.getExtensionObject();

其执行机制为:插件注册时容器会为插件 Bean 附加一个初始化处理器,在插件实例创建时判断其是否实现了 IPluginExtension 接口,若是则调用 getExtensionObject 方法并将返回值缓存到 PluginMeta 的 extensionObject 属性中。该过程仅执行一次,且可以通过 PluginMeta.setExtensionObject 方法预先设置扩展对象,设置后将不再调用接口方法。

插件的卸载与重载​

自 2.1.4 版本开始,插件工厂支持在运行时卸载和重载外部插件,用于实现插件的热拔插:

方法名称描述
unload(pluginId)卸载指定插件,返回 true 表示卸载成功。将依次执行停止插件、移除插件的 Bean 与元数据、关闭插件类加载器、清空宿主侧对该插件的 @PluginRefer 引用
reload(pluginId)重载指定插件,返回 true 表示重载成功。将依据插件所在目录重建类加载器并重新扫描注册其中的类,随后重新初始化并按需启动插件,最后回填宿主侧的 @PluginRefer 引用

示例代码:

IPluginFactory pluginFactory = Plugins.get().getPluginFactory();

// 卸载插件(pluginId 支持插件唯一标识或别名)
if (pluginFactory.unload("echo_plugin")) {
System.out.println("插件已卸载");
}

// 依据卸载时记录的插件目录重新加载插件
if (pluginFactory.reload("echo_plugin")) {
System.out.println("插件已重载");
}

注意事项:

  • 仅支持卸载和重载由 plugin_home 插件目录加载的外部插件(拥有独立类加载器);来源于 CLASSPATH 的插件与宿主共用类加载器,既无法隔离也无法释放,将被直接拒绝并在日志中输出警告
  • reload 支持两种场景:插件仍处于加载状态时的"在线重载",以及插件已被卸载之后的"卸载后重载"(后者的插件目录由卸载过程记录)
  • 卸载时会扫描宿主容器中的全部 Bean,将指向该插件的 @PluginRefer 成员置空;若 Bean 中的其它自定义成员仍持有插件对象,需要自行释放,可通过日志中 [unload-debug] 前缀的输出内容进行排查
  • 卸载/重载会触发 IPluginBeanLoadInitializer.afterBeanUnload 回调,宿主及其它模块可据此清理该插件在运行时注册的资源(如 WebMVC 请求映射、Validation 校验器映射等),防止旧插件类加载器因引用悬挂而无法回收,造成内存泄漏
  • 目前仅提供手动 unload / reload API,重载后在 reload 内部已重新初始化并按需启动插件,故 getPlugin 可直接命中新实例

插件内的Bean注册​

插件除声明插件启动类之外,还可以在插件中使用框架提供的 @Bean、@Interceptor 等注解声明对象:

  • @Plugin 声明的插件启动类注册到插件 Bean 容器中,由插件工厂统一管理
  • 由插件类加载器加载的 @Bean、@Interceptor 对象将注册到宿主 Bean 容器中,因此宿主可以像使用普通 Bean 一样使用插件提供的对象,其中声明的拦截器也将被宿主的 AOP 机制识别

插件 Bean 容器以宿主 Bean 容器作为父容器,因此插件内部同样可以获取宿主中的对象。

插件类加载扩展初始化处理器接口(IPluginBeanLoadInitializer)​

插件模块在初始化时将通过指定的类加载器插件类的扫描,为了能够使插件与其它模块更好的集成,有时候需要在它执行加载逻辑之前需要做一些额外的操作,这时我们可以通过插件类加载扩展初始化处理器(IPluginBeanLoadInitializer) 接口完成,该接口是通过SPI方式在插件模块启动时自动扫描类和包路径下 META-INF/services/ 或 META-INF/services/internal/ 目录中所有名称为 net.ymate.platform.plugin.IPluginBeanLoadInitializer 的文件并加载文件中指定的所有接口实现类。

该接口可以帮助你完成如下操作:

方法名称描述
beforeBeanLoad当插件对象加载器开始执行加载动作前将调用此方法
afterBeanUnload当单个插件被卸载(如单插件热卸载/重载)后将调用此方法,用于清理该插件在各模块运行时注册的缓存资源(可通过 PluginMeta.getClassLoader() 取得其类加载器),避免类加载器被钉死而产生内存泄漏(自 2.1.4 起提供)

其方法参数为当前的插件模块对象(IPlugins)与插件对象加载器(IBeanLoader),因此可以完成诸如注册自定义的注解处理器、追加扫描包名称等扩展操作。

示例代码:

步骤1: 实现接口:

public class DemoPluginBeanLoadInitializer implements IPluginBeanLoadInitializer {

@Override
public void beforeBeanLoad(IPlugins plugins, IBeanLoader beanLoader) {
// 注册自定义注解处理器
beanLoader.registerHandler(DemoAnnotation.class, new DemoAnnotationHandler());
// 追加需要扫描的包名称
beanLoader.registerPackageName("net.ymate.platform.plugin");
}
}

步骤2: 在 META-INF/services/ 目录中添加名称为 net.ymate.platform.plugin.IPluginBeanLoadInitializer 的 SPI 配置文件,内容为实现类的全限定名:

# more META-INF/services/net.ymate.platform.plugin.IPluginBeanLoadInitializer
demo.DemoPluginBeanLoadInitializer

说明:该接口的两个方法(beforeBeanLoad / afterBeanUnload)均已提供默认空实现,实现时按需覆写即可。其中 beforeBeanLoad 在初始化加载前触发,afterBeanUnload 在调用 IPluginFactory.unload / reload 卸载外部插件后触发(详见"插件的卸载与重载"一节)。多个实现类将按照 SPI 的加载顺序依次执行。

插件对象加载器工厂(IPluginBeanLoaderFactory)​

插件的对象加载器由 IPluginBeanLoaderFactory 接口提供,默认实现为 DefaultPluginBeanLoaderFactory,其内部使用一个默认的 DefaultBeanLoader 实例。若需要替换默认的对象加载逻辑,可以实现该接口并通过 @PluginFactory.loaderFactoryClass 或 DefaultPluginConfig.Builder.beanLoaderFactory 指定。

方法名称描述
getPluginBeanLoader返回插件使用的对象加载器