开发 XPocket 插件是一种什么样的体验?原创
前言
报名 XPocket 插件开发已经很久了……
拖到现在才开始写代码的主要原因,还是自己懒,什么工作忙那都是借口。一个集成插件的事,能占用多长时间呢?
这两天重(关)新(掉)振(游)作(戏)之后,我不光写完了插件,还顺手写了篇体验贴。希望能给准备开发、或者开发中的大佬们一些参考。
上手开发
在写代码之前,还是得先看看 XPocket 的开发者指南。
文档写的还是挺简单的,虽然介绍的没那么细,但也大概能看懂,配合 Demo 程序来理解会更简单。
不过都到插件开发了,基本的编程能力肯定是 OK的,开发者文档要还是手把手教学,反而显得有点多余了。
官方 DEMO
文档上也很贴心的,附上了一个官方Demo - XPocket-plugin-example。Demo 非常简单,只有两个类:
- ExampleXPocketCommand
- ExampleXPocketPlugin
ExampleXPocketCommand
这个 Command 类用于处理 XPocket 的命令,同时在类头上通过 @CommandInfo
这个注解来说明该插件的命令定义:
@CommandInfo(name = "example1", usage = "demo command 1", index = 0)
@CommandInfo(name = "example2", usage = "demo command 2", index = 1)
public class ExampleXPocketCommand extends AbstractXPocketCommand {
@Override
public void invoke(XPocketProcess process) throws Throwable {
// 这里很简单,就是直接输出了一下执行的命令和参数,使用 process.output 输出
XPocketProcessTemplate.execute(process,
(String cmd, String[] args) ->
String.format("EXECUTION %s %s",cmd ,
args == null ? null : Arrays.toString(args)));
}
}
注意,**@CommandInfo**
里 name 这个属性是很关键,只有注解修饰的命令才可以使用,不然 XPocket 会直接提示不支持。
最后注解的效果呢,就像下面 top_x 插件的这个样子,name 代表命令名称,而 usage 代表命令的描述。
ExampleXPocketPlugin
Plugin 这个类用于处理一些插件生命周期的工作,比如初始化、销毁、Session相关的。不过我最关心的是输出 LOGO,开发(集成)了一个自己的插件,没有一个炫酷的 LOGO 怎么行!
这里只需要把我们的 LOGO 字符画用 process.output 输出就行了,就这么简单!
/**
* 这个类主要用于插件整体的声明周期管理和日志输出等,如非必要可以不实现
* @author gongyu <yin.tong@perfma.com>
*/
public class ExampleXPocketPlugin extends AbstractXPocketPlugin {
private static final String LOGO = " __ ______ _ _ \n" +
" \\ \\/ / _ \\ ___ ___| | _____| |_ \n" +
" \\ /| |_) / _ \\ / __| |/ / _ \\ __|\n" +
" / \\| __/ (_) | (__| < __/ |_ \n" +
" /_/\\_\\_| \\___/ \\___|_|\\_\\___|\\__|";
/**
* 用于输出自定义LOGO
* @param process
*/
@Override
public void printLogo(XPocketProcess process) {
process.output(LOGO);
}
//...
}
生成字符画的工具有很多啊,这里附上我常用的一个网站 - ASCII Generator,字体宽度之类的都可以自定义,还算方便:
运行官方的 Example 插件
官方提供的这个 Example 是可以直接跑的,我们直接 maven 构建一下,丢到 XPocket 的 plugins 目录:
mvn clean package -Dmaven.test.skip=true
然后将 target/xpocket-plugin-example-2.0.0-RELEASE-jar-with-dependencies.jar
这个 jar 拷贝至 XPOCKET_HOME/plugins ,就是这么简单,插件就安装完成了:
启动 XPocket 后,执行一下 plugins
命令,可以看到插件已经安装成功了:
现在执行插件 - use xpocket-example@XPOCKET
,看看效果:
XPocket 的 shell 做的还是挺好用的,实现了 tab 补全,在敲 use xpocket 之后直接 tab 一下就可以自动补全了,非常方便!
由于我们 Command 类上,注解只添加了 example1/example2
两个 command,所以这里只能用这俩命令测试。先来试一下 example1:
和预期一样,直接输出执行的命令以及参数,啥也没干。
好了,体验完成。可以看到,基本的插件开发还是很简单的,俩类就搞定了。不过拿 Demo 来讲解毕竟还是太糊弄,下面基于我集成的 useful-scripts 插件,来看看完整的插件开发流程是什么样的。
开发插件
我这里开发的插件是 - useful-scripts**,**说白了就是将 useful-scripts 的脚本,都集成到 XPocket 里来。
Command
还是先定义我们可用的 command,这里基于 useful-scripts 仓库里的脚本进行了部分删减,毕竟不是所有脚本都适合放在 XPocket 。
@CommandInfo(name = "coat", usage = "coat /tmp/hello.txt", index = 0)
@CommandInfo(name = "ap", usage = "ap path0 path1 ... pathn", index = 1)
@CommandInfo(name = "rp", usage = "tcp-connection-state-counter", index = 2)
@CommandInfo(name = "tcp-connection-state-counter", usage = "ap path0 path1 ... pathn", index = 3)
@CommandInfo(name = "uq", usage = "uq foo.txt", index = 4)
@CommandInfo(name = "show-busy-java-threads", usage = "show-busy-java-threads", index = 5)
@CommandInfo(name = "show-duplicate-java-classes", usage = "show-duplicate-java-classes", index = 6)
@CommandInfo(name = "find-in-jars", usage = "find-in-jars 'log4j\\.properties'", index = 7)
public class UsefulScriptXPocketCommand extends AbstractXPocketCommand {
}
xpocket.def
需要一个 xpocket.def 文件,就像 JAVA 的MANIFEST 一样,这里需要定义插件的基本信息, XPocket 运行时会读取 plugins 下的所有 jar,解析这个 xpocket.def 文件来加载插件。
plugin-name=useful-scripts
plugin-namespace=KONGWU
plugin-version=0.0.1-SNAPSHOT
main-implementation=com.github.kongwu.xpocket.plugin.usefulscripts.UsefulScriptXPocketPlugin
解压问题
我选择的这个插件很简单,算是 shell 类的插件。简单的说,就是把一堆 shell 脚本集成到 xpocket 里来运行。
怎么执行?
这……倒是个问题,Runtime.exec 肯定也执行不了 Jar 包内的脚本。这个问题在群里和PerfMa 郑伊健(公与)沟通之后,最后定的方案是解压。
在插件启动时,将插件 Jar 包内的 shell 脚本复制到系统上,这样执行的时候只需要通过 sh /path/to/plugin args
就可以完成 shell 脚本的执行了。
不过这里会有一点坑,JAVA 的文件 API 实在是太难用用了,尤其是 nio 包出来之后,新旧两套 API 都存在,导致我最终的解压代码长这样:
private void unpackScripts() {
try {
URI uri = UsefulScriptXPocketCommand.class.getResource("/bin").toURI();
Files.createDirectories(Paths.get(PLUGIN_BIN_PATH));
// 这里fileSystem 虽然没有引用,但这玩意是个很奇怪的设计,全局单例,必须要创建
try (FileSystem fileSystem = (uri.getScheme().equals("jar") ? FileSystems.newFileSystem(uri, Collections.emptyMap()) : null)) {
Path myPath = Paths.get(uri);
Files.walkFileTree(myPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String sourceName = file.getFileName().toString();
Files.copy(file, Paths.get(PLUGIN_BIN_PATH, sourceName), StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
}
有更好方案的大佬,欢迎评论区留言交流,让我这小菜鸡学习学习……
执行脚本
这里简单封装了下 JDK 的 API:
public static String run(String... cmds) throws IOException {
ProcessBuilder pb = new ProcessBuilder(cmds);
pb.redirectErrorStream(true);
Process process = pb.start();
StringWriter sw = new StringWriter();
char[] chars = new char[1024];
try (Reader r = new InputStreamReader(process.getInputStream())) {
for (int len; (len = r.read(chars)) > 0; ) {
sw.write(chars, 0, len);
}
}
return sw.toString();
}
然后只需要拿 process 里的 cmd 和 args 参数,调用 run 方法获取标准输出,传递给 XPocket 就搞定了:
@Override
public void invoke(XPocketProcess process) {
XPocketProcessTemplate.execute(process, (cmd, args) -> OS.run(createExecArgs(cmd, args)));
}
/**
* 创建 exec 参数
* @param cmd useful-scripts cmd
* @param args args
* @return
*/
private String[] createExecArgs(String cmd, String[] args) {
String[] execArgs = new String[args.length + 2];
execArgs[0] = "sh";
execArgs[1] = PLUGIN_BIN_PATH + cmd;
System.arraycopy(args, 0, execArgs, 2, args.length);
return execArgs;
}
Build 处理
目前 XPocket 的插件 Jar 包名称千奇百怪,啥风格的都有:
但这个 jar-with-dependencies 后缀我真是忍不了了……还是调整一下,至少看着像一个正规插件嘛
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>make-assembly-default</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!--固定一下 assembly 包的名称-->
<finalName>${project.artifactId}-fat-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
XPocket 的插件解析策略是,读取 plugins 下所有的 jar,所以我们的程序只能打成 fatjar 的方式来运行,这里就沿用官方 Demo 里的 assembly 插件来构建 fatjar 吧。
测试插件
还是像上面体验 Example 插件那样,将我们的插件复制到 XPOCKET_HOME/plugins 下运行:
可以看到,我们帅气的 LOGO 和命令列表已经打印出来了!
随便执行一个简单的命令测试一下,tcp-connection-state-counter
:
也试试带参数的:
大功告成。
开发过程比我想象中要顺利的多,当然和我这个插件简单也有很大关系,毕竟只是集成一下……前后只花了3小时(还包括参考官方其他插件源码的过程)
版本问题
我这个插件比较简单,并不想刻意的关注版本问题,所以每次 init 解压脚本之前,就直接先清空上一次的解压脚本文件。
不过还是希望以后 XPocket 成熟之后,可以在基础包里提供版本和解压相关的操作,不然每个插件都自己控制解压目录,不得玩炸了。
插件源码
https://github.com/kongwu-/xpocket-plugin-usefulscript/
提交插件
在 XPocket 网站里的插件页面,右上角就有一个上传插件的链接,可以直接上传。因为我这款插件还没完全验证通过,所以暂时就没上传。
说不定再过几天,插件页面里也能看到我的大名了😋
总结 & 建议
XPocket 的插件开发,真的非常简单,毕竟是一个整合的工具,我们要做的只是一个集成,所以自己要做的工作非常少,还没动手的大佬们,赶紧抽时间试试吧。
这里我也提几个改进建议,希望 XPocket 可以更完善:
- 还是
ctrl c
退出的问题,真的很影响体验,用户想要的只是清空那一行 ctrl d
退出会报错- 插件内增加一些统一的功能
- 比如执行 shell 的工具
- 解压的工具(统一目录、版本
- 可以在 XPocket 中,直接用命令安装官方/社区通过审核的插件,而不是自己复制 jar 包