最近开发一个游戏服务器的管理面板,鉴于不同的服务器有不同的功能,所以想通过插件来实现这一逻辑。
在 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;
        });
    }
}
```


      
          使用 pl4j 实现插件系统