巧用Startup簡化Java命令列程式

老錢發表於2018-06-11

最近嘗試了Python語言的開源命令列便捷工具庫Google Fire,它是用來加速使用者編寫命令列程式的一個小工具庫,該工具使用非常方便,節省了編寫命令列程式繁瑣的引數解析程式碼的時間。

巧用Google Fire簡化Python命令列程式

但是我發現Java語言還缺少這樣一個工具庫,於是花了2天時間,將Google Fire移植到了Java語言上,命名為Startup。有了Startup,Java同學也可以從此不用解析繁瑣的命令列引數了。

Startup專案的Github程式碼地址

安裝依賴

<dependency>
	<groupId>com.github.pyloque</groupId>
	<artifactId>startup</artifactId>
	<version>0.0.1</version>
</dependency>
複製程式碼

讓Jedis秒變命令列

import java.io.IOException;

import codehole.startup.Startup;
import redis.clients.jedis.Jedis;

public class Demo {

	public static void main(String[] args) {
		Startup.shell(new Jedis());
	}

}
複製程式碼

使用maven將程式打包成jar包檔案demo.jar,接下來就可以體驗命令列版本的Jedis了。

$ java -jar demo.jar
> @set codehole superhero
OK
> @get codehole
superhero
> @keys *
[codehole]
> @del codehole
1
> @sadd books str[]:java,golang,python
1
> @sadd books str[]:java,golang,python
3
> @smembers books
[python, golang, java]
> @scard books
3
複製程式碼

我們還可以使用單行命令模式,將shell方法改成fire即可,有了fire函式,繁瑣的命令列引數解析過程就可以徹底拜拜了。

Startup.fire(new Jedis(), args);  // 需要傳遞args引數
複製程式碼

接下來,我們嘗試一下單行命令的效果

$ java -jar demo.jar @set codehole superhero
OK
$ java -jar demo.jar @get codehole
superhero
$ java -jar demo.jar @keys "*"
[codehole]
$ java -jar demo.jar @del codehole
1
$ java -jar demo.jar @sadd books str[]:java,golang,python
3
$ java -jar demo.jar @smembers books
[python, golang, java]
$ java -jar demo.jar @scard books
3
複製程式碼

Redis的set命令在Jedis裡存在多個過載函式,很難記清楚具體有哪些引數。不過沒關係,Startup提供了類自省功能,可以列出指定匹配模式的函式呼叫形式。

$ java -jar demo.jar ! @set
class: redis.clients.jedis.Jedis
	method: set(String arg0, String arg1, String arg2)
	method: set(String arg0, String arg1, String arg2, String arg3, int arg4)
	method: set(String arg0, String arg1)
	method: set(String arg0, String arg1, String arg2, String arg3, long arg4)
class: redis.clients.jedis.BinaryJedis
	method: set(byte[] arg0, byte[] arg1, byte[] arg2)
	method: set(byte[] arg0, byte[] arg1, byte[] arg2, byte[] arg3, int arg4)
	method: set(byte[] arg0, byte[] arg1, byte[] arg2, byte[] arg3, long arg4)
	method: set(byte[] arg0, byte[] arg1)
複製程式碼

自省操作符是!,它連帶祖先類一會自省了。當set方法有如此多時,如何呼叫具體哪個set方法呢?Startup使用引數數量和引數型別來匹配函式,如果有多個函式滿足匹配條件,就使用第一個函式。比如現在要呼叫byte[]型別的set函式,我們可以這樣寫

> java -jar demo.jar @set byte[]:1,2,3 byte[]:10,20
OK
> java -jar demo.jar @get byte[]:1,2,3
[B@ae45eb6
複製程式碼

因為輸出是一個byte[]陣列,所以顯示的是它的toString()呼叫的結果。如果使用者不滿意輸出效果,可以通過自定義IConsole.print方法來優化輸出。

Startup.fire(Object target, String[] args, IConsole console);
複製程式碼

細節規則

  1. 方法呼叫使用@字首,如@set
  2. 欄位訪問使用$字首,如$host
  3. 方法呼叫如果遇到同樣引數數量的方法不止一個,那麼在傳遞引數時就必須攜帶引數型別字首資訊,便於startup確定具體應該呼叫哪個方法。如@set str:codehole str:superhero,表示呼叫兩個引數都是String型別的set方法。如果不給予引數型別提示,那麼自動使用第一個找到的方法,這可能不是你想要的。
  4. startup的target引數可以是普通物件,也可以是類。比如Startup.fire(Jedis.class, args),那麼後續的引數作用物件就是Jedis.class。
  5. 如果target是類,那麼可以使用操作符+號對類進行例項化,後面跟著構造器的引數列表。比如Jedis有構造器Jedis(String host, int port),那麼就可以這樣例項化+ str:localhost int:6379
  6. 如果target是類,並且有預設構造器的話,那麼如果要訪問例項方法或者例項欄位的話,就會對這個類進行自動例項化。如果沒有預設構造器,這時候就會丟擲異常。
  7. 如果某個方法返回值為void,Startup會進行特殊處理,返回撥用物件自身。
  8. 自省操作符!可以使用! @顯示所有方法,可以使用! $顯示所有欄位,如果不帶引數,等價於! *列出所有的欄位和方法。像Jedis類裡面例項方法特別多,如果全部列出來會讓人崩潰,這時可以用第三個引數maxlen,表示最多顯示多少行。比如! @ 100最多顯示100行方法。還可以通過模式匹配*號來查詢方法,比如! @*set*匹配所有包行set字串的方法列表。
  9. 輸入exit和q可以退出命令列shell。

例項演示

下面我們使用鏈式呼叫的例子來演示Startup的強大威力。首先我們編寫一個Counter類,可以對該類裡面的整形欄位value進行incr和decr操作。然後使用Startup對它進行shell化。同時我們定一個coeff靜態變數,用於放大自增自減的效果。

import codehole.startup.Startup;

public class Demo {

	static class Counter {
		int value;
		static int coeff = 1;

		public Counter() {
			this(10);
		}

		public Counter(int value) {
			this.value = value;
		}

		public static int coeff() {
			return coeff;
		}
		
		public static void coeff(int c) {
			coeff = c;
		}

		public Counter incr() {
			return incr(1);
		}

		public Counter incr(int v) {
			this.value += v * coeff;
			return this;
		}

		public Counter decr() {
			return incr(1);
		}

		public Counter decr(int v) {
			this.value -= v * coeff;
			return this;
		}

		public int value() {
			return value;
		}
		
		public String toString() {
			return String.format("Counter(value=%d)", value);
		}

	}

	public static void main(String[] args) {
		Startup.shell(Counter.class); // 注意,這裡的目標是類
	}

}
複製程式碼

我們執行一下,體驗一下Startup的神奇魅力。

自省

$ java -jar demo.jar
# 列出所有方法
> ! @
class: codehole.startup.demo.Demo.Counter
	method: value()
	method: decr()
	method: decr(int arg0)
	method: static coeff(int arg0)
	method: static coeff()
	method: incr(int arg0)
	method: incr()
# 列出所有欄位
> ! $
class: codehole.startup.demo.Demo.Counter
	field: int value
	field: static int coeff
# 列出所有的incr方法
> ! @incr
class: codehole.startup.demo.Demo.Counter
	method: incr(int arg0)
	method: incr()
# 列出所有的decr方法
> ! @decr
class: codehole.startup.demo.Demo.Counter
	method: decr()
	method: decr(int arg0)
# 列出字尾為cr的方法
> ! @*cr
class: codehole.startup.demo.Demo.Counter
	method: decr()
	method: decr(int arg0)
	method: incr(int arg0)
	method: incr()
複製程式碼

構造器

# 預設構造器
> +
Counter(value=10)
# 呼叫構造器Counter(int)
> + 5
Counter(value=5)
# 構造完繼續獲取例項欄位
> + $value
10
> + 5 $value
5
# 構造完繼續呼叫例項方法
> + @value
10
> + 5 @value
5
# 對類[訪問例項欄位或呼叫例項方法]會自動構造該類的預設例項,再回撥方法或者訪問例項欄位
> $value
10
> @value
10
複製程式碼

靜態欄位和方法

# 對類呼叫靜態方法和對例項呼叫靜態方法效果是一樣的
> @coeff
1
> + @coeff
1
# 對類訪問靜態欄位和對例項訪問靜態欄位效果是一樣的
> $coeff
1
> + $coeff
1
# 返回型別為void的方法會自動調整返回結果為呼叫物件
> @coeff 2
class codehole.startup.demo.Demo$Counter
> + @coeff 2
Counter(value=10)
> @coeff 2 $coeff
2
複製程式碼

鏈式呼叫

> @incr @incr @incr 5 @incr 5 $value
22
> @incr 5 @decr 5 @incr 5 @decr 5 $value
10
> + @incr @incr @incr 5 @incr 5 $value
22
> + @incr 5 @decr 5 @incr 5 @decr 5 $value
10
複製程式碼

總結

Java版本的Startup和Google Fire使用上還是有一些不一樣,這個是語言的機制差別所致。但是使用上是大同小異,基本上Google Fire能做到的事,Startup都能做了,只是在使用形式上略有不同。

Startup繼承了Fire的優良傳統,小巧輕便沒有任何依賴項,使用一行程式碼就把命令列的繁瑣解析過程給徹底消滅了。同時還提供了Fire所欠缺的自省功能和互動式shell,使用者使用startup進行除錯工作時會無比輕鬆。

閱讀更多高階文章,關注微信訂閱號「碼洞」

相關文章