【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 之外的資料甚少,就不贅述。

結語

因爲我也在學習當中。歡迎交流!上述有什麼說法不當的,也還請各位大牛不吝指正!

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