【REPL】(三)Felix Gogo —— 可能是 Java 下最完美的CLI框架

背景

这个框架是在浏览 Bndtools Getting Started 的时候无意中发现的。Bndtools 嘛,构建 OSGi 应用的一套工具。
Eclipse 基于 OSGi,很多 bug 也都和 OSGi 有关。由此我对 OSGi 这个其貌不扬的框架产生了兴趣。
只有充分了解了 OSGi,才能够更好定位插件及RCP开发过程中的那些莫名其妙的错误吧……
目前有很大的想法,绕开 Eclipse 繁琐的插件安装机制,直接在 Eclipse 以外使用它的部分组件以加速启动速度,节约内存。既然说 OSGi 有热插拔机制,理论上讲这不成问题……
目前已经快定位到 Eclipse 的启动机制了,想 Programmatically start an Eclipse IApplication 的人虽然少但还是有的。目前其他参数的简单接口实现已经在这篇文章中给出来了,唯独一个IApplicationContext接口不知道该如何实现,还有ApplicationDescriptor.launch(Map)这一步的Map怎么填才不会启动失败,可能是我们平时看不到的一些 Eclipse 启动参数,其可执行文件是为我们配置好了的,我们自己配的话,就需要花一点功夫琢磨。

回归正题

Bndtools 教程给出的图文并茂的描述来看,这个 Gogo 框架还是很强大的,主要体现在:

  • 自己实现了不少系统命令,如lscd等,虽然经过初步测试(用的是旧版最后一版 felix,新版问题已经修复,可以不往后看),没有见到任何输出(事实证明是先cd ..将我带入了..子目录,再ls肯定没有什么输出,echo $PWD可以看到我位于一个名为..的子目录中却不会报错;先ls的话是有输出的,只能说是因为 Gogo 不能认出..这个相对路径的含义)。
  • 怦然心动的install命令,据help讲能够install bundle using URLs,和 OSGi 热插拔的特性完美结合,在此基础上开发出类系统包管理器的 CLI 版 Eclipse,可以自成系统生态。Java 也能像 Python pip、Ruby gem 那样管理不是梦。
  • 有避免来自不同 Bundle 的命令冲突的机制,即在命令前加上类似包名的标识符,如gogo:echo,第一眼看上去跟 Java 又长又臭的风格差不多,但是仔细思考一下就觉得这个机制相当不错啊,在繁琐和简约之间做了最佳的平衡,平时既可以输入简单的echo,也能输入gogo:echo,取决于是否需要 disambiguation。
  • 有自己的一套现代编程语言风格的 bash,能够像各种编程语言的官方 REPL shell 一样在命令行做简单的实时运算,你能像 bash 一样echo Hello | tac temp.txt,也能来一段现代静态的nr = new java.lang.Integer 10,这是建立在静态语言之上的动静态语言交相辉映的系统。
  • nr = new java.lang.Integer 10[a=1 b=2 c=3] get b也可以看出,Gogo 实际上将 Java 方法 bash 化了。莫名喜感的是退出 CLI 的命令可能让你一时半会摸不着头脑,出乎你的意料,却又在情理之中:exit 0,初始 Gogo 只有systemgogo两个包,不难想象这句命令是 Java System.exit(0)的 bash 化写法;类 bash 写法的现代编程语言也有 Ruby,参见此例,但是和 Ruby 相比却省下了不少逗号;这也为 Java 方法与 bash 命令的桥接奠定了基础。实际上,教程介绍了如何在 Eclipse 中利用 Bndtools 为 Gogo 添加新的命令。从 Java 类编写与 annotation 标注开始,到变成一句可识别的 bash 命令,无缝衔接。
  • 更加难能可贵的是,还有命令语法检查、高亮、补全功能,不存在的命令事先给出红色字体,有效的命令给出蓝色字体。
  • 当然,我非常关注的历史命令回溯能力也都有。

这么多的功能,系统管理和软件编程能力实现与整合程度如此之高,我已经很难再挑剔什么,借助 Java 生态做 CLI 应用潜力巨大。
旧的官方仓库(felix)已经被归档于archived分支,新的仓库(felix-dev)在这里。现在克隆下来,进入main文件夹。
这里并不是按照 Gogo 文档那样仅仅进入gogo文件夹maven clean install。其文档也说了:

Gogo is included as the default shell in the felix framework distribution.

执行方法为$ java -jar bin/felix.jar,构建gogo可能只得到gogo.jar,只有将整个 Felix 工程都构建才能得到main/bin/felix.jar。实际上,仅仅对gogo文件夹进行构建会提示在 test 目标失败,给出的原因仅仅是NullPointerException,debug 让你怀疑人生。

[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.074 s
[INFO] Finished at: 2021-01-03T08:54:52+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.20.1:test (default-test) on project org.apache.felix.gogo.runtime: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.20.1:test failed.: NullPointerException -> [Help 1]

进入main文件夹,执行maven clean install,提示错误:

[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.693 s
[INFO] Finished at: 2021-01-02T16:20:40+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project org.apache.felix.main: Could not resolve dependencies for project org.apache.felix:org.apache.felix.main:bundle:6.1.0-SNAPSHOT: Could not find artifact org.apache.felix:org.apache.felix.framework:jar:6.1.0-SNAPSHOT in aliyunmaven (https://maven.aliyun.com/repository/public) -> [Help 1]

这是由于你配置的 Maven 仓库已经没有7.1.0-SNAPSHOT这个版本,到写本文之时最新版本为7.0.0,需要对这个版本好出现的所有地方进行修改,仅仅做这一步修改就构建成功了,也没有单单构建gogo项目所提示的 test 目标出错。
最后,执行$ java -jar bin/felix.jar。Enjoy! 具体如何拓展命令,请看 Bndtools Getting Started

解剖

在探索如何不借助 Bndtools 对命令进行拓展,从而打造轻量级编辑环境的过程中发现了gogo目录下jline子目录,最终发现 Gogo 的功能核心原来是 JLine。众里寻他千百度,蓦然回首,JLine 却在灯火阑珊处。如此正式如此好用的 CLI 框架,居然没能够百度和必应出来!我们来看各 CLI 框架的自我介绍:
| 框架 | 英文介绍 | 中文介绍 |
|:----|:----|:----|
|python-nubia | command-line and interactive shell framework | -
| Python Prompt Toolkit | library for building interactive command line applications, a replacement for GNU readline | 构建交互式命令行的库
| JCommander | framework that parse command line parameters | 命令行参数解析库
| Text-IO | library for creating console applications, used in applications that need to read interactive input from the user | -
| JLine | library for handling console input, similar in functionality to BSD editline and GNU readline | 处理控制台输入的类库
还是有些差别的,我觉得 JLine 这类叫做 CIL(console input library,控制台输入类库)而不是 CLI 合适。记下来开始尝试移植:
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.gedobu.some</groupId>
    <artifactId>some.osgi</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Main-Class>cn.gedobu.some.osgi.Main</Main-Class>
                                        <X-Compile-Source-JDK>${maven.compile.source}</X-Compile-Source-JDK>
                                        <X-Compile-Target-JDK>${maven.compile.target}</X-Compile-Target-JDK>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.apache.felix.gogo.jline</artifactId>
            <version>1.1.8</version>
        </dependency>

    </dependencies>
</project>

仅仅使用1个依赖,2个插件一个用来设置运行环境,一个用来打包成 jar。
Main.class

public class Main {

    public static void main(String[] args) throws IOException {
        Terminal terminal = TerminalBuilder.builder().name("gogo")
                .system(true)
                .nativeSignals(true)
                .signalHandler(Terminal.SignalHandler.SIG_IGN)
                .build();

        ThreadIOImpl tio = new ThreadIOImpl();
        tio.start();
        try {
            CommandProcessorImpl processor = new CommandProcessorImpl(tio);
            org.apache.felix.gogo.jline.Shell.Context context = new org.apache.felix.gogo.jline.Shell.Context() {
                public String getProperty(String name) {
                    return System.getProperty(name);
                }

                public void exit() {
                    System.exit(0);
                }
            };
            Shell shell = new Shell(context, processor, tio, null);
            processor.addCommand("gogo", processor, "addCommand");
            processor.addCommand("gogo", processor, "removeCommand");
            processor.addCommand("gogo", processor, "eval");
            processor.addConverter(new BaseConverters());
            register(processor, new Builtin(), Builtin.functions);
            register(processor, new Procedural(), Procedural.functions);
            register(processor, new Posix(processor), Posix.functions);
            register(processor, shell, Shell.functions);
            InputStream in = new FilterInputStream(terminal.input()) {
                @Override
                public void close() {
                }
            };
            OutputStream out = new FilterOutputStream(terminal.output()) {
                @Override
                public void close() {
                }
            };
            CommandSession session = processor.createSession(in, out, out);
            session.put(Shell.VAR_CONTEXT, context);
            session.put(Shell.VAR_TERMINAL, terminal);
            try {
                String[] argv = new String[args.length + 1];
                argv[0] = "--login";
                System.arraycopy(args, 0, argv, 1, args.length);
                shell.gosh(session, argv);
            } catch (Exception e) {
                Object loc = session.get(".location");
                if (null == loc || !loc.toString().contains(":")) {
                    loc = "gogo";
                }

                System.err.println(loc + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
                e.printStackTrace();
            } finally {
                session.close();
            }
        } finally {
            tio.stop();
        }
    }

    static void register(CommandProcessorImpl processor, Object target, String[] functions) {
        for (String function : functions) {
            processor.addCommand("gogo", target, function);
        }
    }
}

关键在这几行代码:

register(processor, new Builtin(), Builtin.functions);
register(processor, new Procedural(), Procedural.functions);
register(processor, new Posix(processor), Posix.functions);
register(processor, shell, Shell.functions);

这些私有的 functions 属性我们无法访问,只能通过复制源码,使用自己的类。然后我们的轻量级 Gogo 便从庞大的 Felix 框架中分离好了。
同时可以在 Shell 类中注意到,其 Context 并不要求exit命令提供一个整数参数,源码中仅仅在org.apache.felix.framework.util包下发现了下面这段代码要求提供一个整数参数:

case SYSTEM_EXIT_ACTION:
    System.exit(((Integer) arg1).intValue());

因此,我们分离出来的 CLI 是仅仅用exit来退出的。

Felix 功能拓展的源头

鉴于上述的 Gogo JLine 仅仅实现了整个 Felix console 的部分命令,那么在main模块中的诸如installlist这些命令到底来自哪里?有必要深入研究main这个模块。
main模块的启动方式很明晰,一个public static void main(String[] args) throws Exception函数就能启动,这也在其pom.xml中得到印证:

<Main-Class>org.apache.felix.main.Main</Main-Class>

然而,在一个单独导入了org.apache.felix.main包的项目中以这种方式启动,只能看到一个没有任何输出、无法输入命令进行交互、无法自行停止 的空程序。是缺少了什么代码配置吗?其实不是。将main模块生成的bundle文件夹删除,你会发现此时的 felix 状态和我们在单独项目中看到的一致,说明 felix.main.Main 会识别特定的文件结构,从识别到的文件结构中自动加载所有的 OSGi Bundle 扩展。这个文件结构是:

main
├── bin
│   └── felix.jar
├── bundle
│   ├── jansi-1.18.jar
│   ├── jline-3.13.2.jar
│   ├── org.apache.felix.bundlerepository-2.0.10.jar
│   ├── org.apache.felix.gogo.command-1.1.2.jar
│   ├── org.apache.felix.gogo.jline-1.1.8.jar
│   └── org.apache.felix.gogo.runtime-1.1.4.jar
├── conf
│   └── config.properties
└── felix-cache
    ├── bundle0
    │   ├── bundle.id
    │   └── last.java.version
    ├── bundle1
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    ├── bundle2
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    ├── bundle3
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    ├── bundle4
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    ├── bundle5
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    ├── bundle6
    │   ├── bundle.info
    │   └── version0.0
    │       ├── bundle.jar
    │       └── revision.location
    └── cache.lock

结合源码可知,felix.jar 包运行后,查看了自己所在的目录,用了查找定位字符串并截取的办法,因此,main模块生成的 jar 包必须要重命名为 felix.jar,否则不能找到自己所在的位置。顺着这个位置,jar 包进入其上级目录,再进入conf子目录寻找配置文件config.properties,进入bundle子目录加载里面所有的 OSGi Bundle,生成felix-cache文件夹,产生缓存文件后,才形成了我们所见的既有 POSIX 命令、又有个性命令,还能进行 gosh 编程的模样。其中必需的文件有:

  • 配置文件config.properties(决定 felix.jar 是否会自动读取bundle目录的 Bundle)
  • bundle目录下的jline-*.jarorg.apache.felix.gogo.jline-*.jarorg.apache.felix.gogo.runtime-*.jar这些核心 Bundle(删除必报错)
  • org.apache.felix.gogo.command-*.jar(提供install命令)
  • org.apache.felix.bundlerepository-*.jar(提供list,列出已加载 Bundle 列表的命令)

Jansi 是 Java 中让控制台输出彩色字符的方法,对功能影响不大;而 Jmood,据 OSGi 官方所述:

Jmood is a JMX-based OSGi management agent

除了这个 pdf 之外的资料甚少,就不赘述。

结语

因为我也在学习当中。欢迎交流!上述有什么说法不当的,也还请各位大牛不吝指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章