性能文章>开发 XPocket 插件是一种什么样的体验?>

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

https://a.perfma.net/img/2521381
9月前
4271614

前言

报名 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 <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,字体宽度之类的都可以自定义,还算方便:

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 包

参考

请先登录,再评论

厉害!

8月前

太牛逼了

8月前

有点内个味了👍

9月前

大佬🐂🍺,我们还在观望,你已经上手写了

19月前
回复 天下无双:

赶紧写一个试试

9月前回复

这个分享很棒

19月前

yyds

9月前

为你推荐

JVM 源码分析之一个 Java 进程究竟能创建多少线程
概述虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从 JVM 源码角度来分析,更多的来自于 Linux Kernel 的源码分析,今天要说的是 JVM 里比较常见的一个问题。这个问题可能有
线程池运用不当的一次线上事故
在高并发、异步化等场景,线程池的运用可以说无处不在。线程池从本质上来讲,即通过空间换取时间,因为线程的创建和销毁都是要消耗资源和时间的,对于大量使用线程的场景,使用池化管理可以延迟线程的销毁,大大提高
YGC问题排查,又让我涨姿势了!
在高并发下,Java程序的GC问题属于很典型的一类问题,带来的影响往往会被进一步放大。不管是「GC频率过快」还是「GC耗时太长」,由于GC期间都存在Stop The World问题,因此很容易导致服务
【活动结果公布:兑奖见置顶评论】每日一签到,解锁你的PerfMa壕礼
叮......Perfma达人福利发车啦!社区每日签到解锁活动上线啦~除了每日签到必得积分奖励外,还为大家增加了壕礼相赠、解锁Perfma终极大奖等环节奖励 ~来Perfma社区留下你的签到足迹,告诉
使用Top_X插件排查内存过载问题
Top命令是Linux 系统下常用的监控工具,用于实时获取进程级别的 CPU 或内存使用情况。XPocket中的Top_X为Linux Top的增强版,可以显示CPU占用率/负载,CPU及内存进程使用
记一次Synchronized关键字使用不合理,导致的多线程下线程阻塞问题排查
在为客户进行性能诊断调优时,碰到了一个Synchronized关键字使用不合理导致多线程下线程阻塞的情况。用文字记录下了问题的整个发现-排查-分析-优化过程,排查过程中使用了我司商业化产品——XLan
记一次类加载失败导致线程阻塞问题排查
作为PerfMa解决方案管理部门的技术专家,我在工作遇见过很多各种问题导致的性能问题,并参与了为客户的系统进行性能诊断调优的全过程。这一次碰到了一个类加载失败导致的性能问题。用文字记录下了问题的整个发
一次大量 JVM Native 内存泄露的排查分析(64M 问题)
我们有一个线上的项目,刚启动完就占用了使用 top 命令查看 RES 占用了超过 1.5G,这明显不合理,于是进行了一些分析找到了根本的原因,下面是完整的分析过程,希望对你有所帮助。会涉及到下面这些内容Linux 经典的 64M 内存问题堆内存分析、Native 内存分析的基本套路