9

开发 XPocket 插件是一种什么样的体验?

 2 years ago
source link: https://www.heapdump.cn/article/2651576
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
空无3天前

报名 XPocket 插件开发已经很久了……

拖到现在才开始写代码的主要原因,还是自己懒,什么工作忙那都是借口。一个集成插件的事,能占用多长时间呢?

image.png

这两天重(关)新(掉)振(游)作(戏)之后,我不光写完了插件,还顺手写了篇体验贴。希望能给准备开发、或者开发中的大佬们一些参考。

在写代码之前,还是得先看看 XPocket 的开发者指南

文档写的还是挺简单的,虽然介绍的没那么细,但也大概能看懂,配合 Demo 程序来理解会更简单。

不过都到插件开发了,基本的编程能力肯定是 OK的,开发者文档要还是手把手教学,反而显得有点多余了。

官方 DEMO

文档上也很贴心的,附上了一个官方Demo - XPocket-plugin-example。Demo 非常简单,只有两个类:

  1. ExampleXPocketCommand
  2. 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 代表命令的描述。

image.png

ExampleXPocketPlugin

Plugin 这个类用于处理一些插件生命周期的工作,比如初始化、销毁、Session相关的。不过我最关心的是输出 LOGO,开发(集成)了一个自己的插件,没有一个炫酷的 LOGO 怎么行!

这里只需要把我们的 LOGO 字符画用 process.output 输出就行了,就这么简单!

/**
 * 这个类主要用于插件整体的声明周期管理和日志输出等,如非必要可以不实现
 * @author gongyu <[email protected]>
 */
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,字体宽度之类的都可以自定义,还算方便:

image.png

运行官方的 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 命令,可以看到插件已经安装成功了:

image.png

现在执行插件 - use xpocket-example@XPOCKET,看看效果:

image.png

XPocket 的 shell 做的还是挺好用的,实现了 tab 补全,在敲 use xpocket 之后直接 tab 一下就可以自动补全了,非常方便!

由于我们 Command 类上,注解只添加了 example1/example2 两个 command,所以这里只能用这俩命令测试。先来试一下 example1:

image.png

和预期一样,直接输出执行的命令以及参数,啥也没干。

好了,体验完成。可以看到,基本的插件开发还是很简单的,俩类就搞定了。不过拿 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 里来运行。

image.png

怎么执行?

这……倒是个问题,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 包名称千奇百怪,啥风格的都有:

image.png

但这个 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 下运行:

image.png

image.png

可以看到,我们帅气的 LOGO 和命令列表已经打印出来了!

随便执行一个简单的命令测试一下,tcp-connection-state-counter

image.png

也试试带参数的:

image.png

大功告成。

开发过程比我想象中要顺利的多,当然和我这个插件简单也有很大关系,毕竟只是集成一下……前后只花了3小时(还包括参考官方其他插件源码的过程)

我这个插件比较简单,并不想刻意的关注版本问题,所以每次 init 解压脚本之前,就直接先清空上一次的解压脚本文件。

不过还是希望以后 XPocket 成熟之后,可以在基础包里提供版本和解压相关的操作,不然每个插件都自己控制解压目录,不得玩炸了。

https://github.com/kongwu-/xpocket-plugin-usefulscript/

XPocket 网站里的插件页面,右上角就有一个上传插件的链接,可以直接上传。因为我这款插件还没完全验证通过,所以暂时就没上传。

image.png

说不定再过几天,插件页面里也能看到我的大名了😋

总结 & 建议

XPocket 的插件开发,真的非常简单,毕竟是一个整合的工具,我们要做的只是一个集成,所以自己要做的工作非常少,还没动手的大佬们,赶紧抽时间试试吧。

这里我也提几个改进建议,希望 XPocket 可以更完善:

  1. 还是ctrl c 退出的问题,真的很影响体验,用户想要的只是清空那一行
  2. ctrl d退出会报错
  3. 插件内增加一些统一的功能
    1. 比如执行 shell 的工具
    2. 解压的工具(统一目录、版本
  4. 可以在 XPocket 中,直接用命令安装官方/社区通过审核的插件,而不是自己复制 jar 包

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK