最近开发一个游戏服务器的管理面板,鉴于不同的服务器有不同的功能,所以想通过插件来实现这一逻辑。
在 java 实现插件也不是很复杂,主要就是像 tomcat 一样,使用一个单独的ClassLoader,加载实现了特定接口的类,我使用了 [pl4j](https://pf4j.org/) 来实现这一功能,这个库本身也是足够轻量简单,也足够灵活,省的自己再去写一系列加载逻辑了。
---
库的使用
```java
public class WelcomePlugin extends Plugin {
public WelcomePlugin(PluginWrapper wrapper) {
super(wrapper);
}
}
```
继承 Plugin 类,打成 jar 包,ja r包中要有以下内容的 `MANIFEST.MF`
```properties
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: decebal
Build-Jdk: 1.6.0_17
Plugin-Class: org.pf4j.demo.welcome.WelcomePlugin
Plugin-Dependencies: x, y, z
Plugin-Id: welcome-plugin
Plugin-Provider: Decebal Suiu
Plugin-Version: 0.0.1
```
然后你的插件就可以通过 `PluginManager` 的 `load` 方法加载了;
插件肯定是要实现对应功能的,你可以使用 pl4j 的 `Extension` 概念,通过外部定义一个继承 `ExtensionPoint `的接口,在插件中实现它,就可以通过
`getExtensions` 方法获取该接口在插件中的实现。
Plugin 类有个构造器接受 `PluginWrapper` 的传参,`PluginWrapper` 描述了该插件的各种信息,但官方更推荐你创建个自己的插件类继承 Plugin,使用自定义的上下文传参,然后自定义 `PluginFactory` ,
通过自定义 `PluginFactory` ,可以自定义插件类实例化方式,实例化之前的上下文初始化,自定义传参等各种行为。
---
库的使用还是比较简单的,分享下我需求的实现。我的需求很简单,这些插件负责实现各自的功能,然后有些可以提供界面,界面的展示是标签页的形式显示在导航栏上。
首先是自定义一个 `BasePlugin` 类,其他插件都要继承这个类,然后使用了自定义上下文
```java
public class BasePlugin extends Plugin {
protected PluginContext pluginContext;
public BasePlugin(PluginContext context) {
this.pluginContext = context;
}
}
```
自定义上下文主要分为以下几个内容
```java
public class PluginContext {
public final PluginController controller;
public final PluginDescriptor descriptor;
public final PluginParams params;
public final PluginStorage storage;
public PluginContext(PluginController controller, PluginDescriptor descriptor, PluginParams params, PluginStorage storage) {
this.controller = controller;
this.descriptor = descriptor;
this.params = params;
this.storage = storage;
}
}
```
* controller 负责注册插件和外界进行的交互
* descriptor 可以获取到当前插件信息
* params 当前插件的启动参数
* storage 当前插件的文件读写相关,统一管理
其他三个没什么好说的,主要讲下 `controller` 里面
```java
public interface PluginController {
/**
* 注册页面
*/
void registerPage(PageDescriptor descriptor, PageSupplier pageSupplier);
/**
* 注册资源处理器
*/
void registerResourceHandler(ResourceSupplier resourceSupplier);
/**
* 注册请求处理程序
*
* @param descriptor 操作描述符
* @param processor 请求处理器
*/
void registerAction(ActionDescriptor descriptor, ActionProcessor processor);
/**
* 注册事件监听器
*
* @param eventId 事件id
* @param processor 事件处理器
*/
void registerEventHandler(String eventId, EventProcessor processor);
/**
* 发送事件
*
* @param event 事件
* @return 事件序列号
*/
String emitEvent(SendEvent event);
/**
* 创建任务
*
* @param taskData 任务数据
* @return 创建的任务
*/
AsyncTask createTask(TaskData taskData);
/**
* 获取当前插件的上下文
*
* @return 插件请求路径上下文
*/
String getContextPath();
}
```
这里的设计比较奇葩,用于处理插件 web 界面显示还有其他东西,没有像 spring 那种设计,也没有像 servlet 那种,我觉得这种和框架无关,并且足够简单的设计,后期也比较好扩展,实现也简单。只要插件内容不要太大,开发也不会变复杂。
注册页面就是注册在管理面板 tabs 页面显示的每个 tab ,点击后就会请求插件提供页面内容,也就是iframe的内容。
例如 `{serverId}/page/{页面id}`,页面id是全局唯一的
而资源就是 `{服务器id}/plugin/{插件id}/res/{资源id}` ,资源是例如css,image,甚至下载文件等
action 就是 `{服务器id}/plugin/{插件id}/action/{操作id}`,action 就是 http 请求,类似 rpc 调用。
简单说下就是下面几个概念
* page:全局唯一,每个页面一个 tab 页,用于展示给用户,也可以没有
* resource: 资源,可以是网页资源,或者其他
* action: 请求的操作,可以是网页请求的,也可以是来自定时任务的调用,每个插件内有唯一的 actionId
* event:事件,可以发送和处理事件,事件id全局唯一,发送事件时,可以指定事件的传播范围
* task: 长时间的任务,批量处理什么的,可以在前端显示进度条给用户
大概就是这样,看下具体一个插件的实现
```java
public class DemoPlugin extends BasePlugin {
private FreemarkerUtil freemarkerUtil;
private InputStream loadStatic(String path) {
try {
return this.getClass().getClassLoader().getResource("static/" + path).openStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String getContentTypeByUrl(String path) {
if (path.endsWith("css")) {
return "text/css; charset=utf-8";
} else if (path.endsWith("js")) {
return "text/javascript; charset=utf-8";
} else {
return "text/txt";
}
}
private String loadTemplate(String page, Map<String, Object> params) {
try {
return freemarkerUtil.loadTemplate(page, params);
} catch (IOException | TemplateException e) {
throw new RuntimeException(e);
}
}
public DemoPlugin(PluginContext context) {
super(context);
freemarkerUtil = new FreemarkerUtil(this.getClass(), "/pages/");
PluginController controller = context.controller;
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("baseUrl", controller.getContextPath());
controller.registerPage(PageDescriptor.builder()
.id("demo-page")
.name("演示界面")
.description("用于演示插件功能和基本操作")
.permissions("demo")
.build(),
() -> loadTemplate("demo.ftlh", templateParams));
controller.registerResourceHandler((resourceId) -> {
ResourceResult result = new ResourceResult();
result.setDataProvider(loadStatic(resourceId));
result.setContentType(getContentTypeByUrl(resourceId));
return result;
});
}
}
```
![image.png](https://thetbw-hk.cos.thetbw.xyz/blog/image_1714297441048.png)
![image.png](https://thetbw-hk.cos.thetbw.xyz/blog/image_1714297454489.png)
使用 pl4j 实现插件系统