程式碼高𠅙

2011/09/06

用 Java Annotation 簡化命令列參數處理

撰寫 Java 命令列程式,免不了要處理命令列參數。常見的作法是跑個 for loop, 裡面用 if ... else 去判斷要執行的選項。這樣的作法當然可以達到目的,不過比較不具有程式的美感。

其實在 Web 上,同樣的問題也存在,就是怎麼把 URL 對應到特定的 servlet, action 或 controller 的方法上。這不管是在 Spring Framework 3 Web MVC, Servlet 3 或是 Jessey 中,都有一套近似的解法,就是使用 @Annotation。

OK, 那我們何不如法泡製,也用 @Annotation 來處理命令列參數呢? 我設計的命令列規範如下:
  • 將命令列參數區分成「選項」及「選項參數」。選項如 -start, -stop;選項參數如 -start localhost 18080 中,未加 dash 符號的字串,即紅字部分。
  • 一個命令列可以接受多個選項及多個選項參數。
  • 支援將選項綁定到主程式裡面的方法,以下我稱之為「選項常式」。
  • 支援將選項參數綁定到選項常式的參數上。
  • 支援自動列出所有選項及其說明。
  • 自動檢查選項參數是否正確。 
  • 支選整數型態的選項參數。
基於這個規範,我設計出的這組 Command line API,可以透過類似以下方式使用:

package com.isong.cmdexe;

/**
 * Usage example:<pre>
 * java -jar ds.jar -start localhost 2011
 * java -jar ds.jar -start localhost 2011 -sm
 * java -jar ds.jar -stop
 * </pre>
 * @author edwardsayer
 */
public class DummyServer {
    /**
     * @param args
     */
    public static void main(String[] argv) {
        new CmdExecutor(new DummyServer()).exec(argv);
       
    }
   
    // execute: java -jar ds.jar -start localhost 2011
    @CommandArg(value = "-start ip port", desc = "start the server instance.")
    public void doStart(String ip, int port){
        System.out.println("Starting server at " + ip + ":" + port + "...");
    }
   
    // execute: java -jar ds.jar -sm 
    @CommandArg(value = "-sm", desc = "start session monitor.")
    public void startSessionMonitor(){
        System.out.println("Starting session service...");
    }
   
    // execute: java -jar ds.jar -stop 
    @CommandArg(value = "-stop", desc = "stop the server instance.")
    public void doStop(){
        System.out.println("Stoping...");
    }
}

從以上的程式碼片段,可以知道這組 Command line API 提供了一個 CmdExecutor 類別, 以及一個 @CommandArg annotation。

@CommandArg 會在主程式的選項常式上,加上標注訊息,包括選項名稱,以及選項描述。

而CmdExecutor 類別則依據 @CommandArg 所提供的資訊,綁定使用者輸入的參數。另外,若使用者未輸入任何參數,CmdExecutor 則顯示參數提示訊息,這些訊息,其實是我們透過 @CommandArg 加諸在每個選項常式上的訊息。範例如下:

Available options:
-start ip port
    start the server instance.
-sm
    start session monitor.
-stop
    stop the server instance.

接下來,就來了解 CmdExecutor 及 @CommandArg 的實作方式。先看看 @CommandArg 的程式碼:

@Retention(RetentionPolicy.RUNTIME)
public @interface CommandArg {
    String value();
    String desc() default "";
}

可以看到 @CommandArg 包含兩個屬性。value 用來設定選項名稱,而 desc 用來設定選項描述。

CmdExecutor 較為冗長,程式碼如下:

public class CmdExecutor {
    private Map cmdMap;
    private StringBuffer cmdDocs;
    private Object target;
    
    public CmdExecutor(Object target){
        this.target = target;
    }
    
    public int exec(String[] args){
        System.out.println("args = " + Arrays.asList(args));
        initCommands();
        if (args.length == 0) {
            showCommands();
            return 0;
        }
        
        for (int i = 0; i < args.length; i++) {
            if (args[i].startsWith("-")) {
                String option = args[i];
                Method method = cmdMap.get(option);
                if (method == null) {
                    System.out.println("Unknown options: " + option);
                    showCommands();
                    return 1;
                }
                try {
                    Class[] types = method.getParameterTypes();
                    Object[] paras = new Object[types.length];
                    for (int j = 0; j < types.length; j++) {
                        if ( ++i >= args.length || args[i].startsWith("-")) {
                            System.out.println("Invalid option parameter count: " + option);
                            showCommands();
                            return 1;
                        }
                        if(types[j].isAssignableFrom(String.class))
                            paras[j] = args[i];
                        else if(types[j].isAssignableFrom(int.class) 
                                || types[j].isAssignableFrom(Integer.class))
                            paras[j] = Integer.valueOf(args[i]);
                        else
                            throw new IllegalArgumentException("Invalid command parameter type: " + method.getName());
                    }
                    method.invoke(target, paras);
                } catch (Exception e) {
                    e.printStackTrace();
                } 
            }
        }
        return 0;
    }
    
    private void initCommands(){
        if (cmdDocs != null)
            return;
        
        cmdDocs = new StringBuffer("Available options:\n");
        cmdMap = new HashMap();
        Method[] methods = target.getClass().getMethods();
        for (Method method: methods) {
            CommandArg argCmd = method.getAnnotation(CommandArg.class);
            if (argCmd != null) {
                cmdDocs.append(argCmd.value() + "\n    " + argCmd.desc() + "\n");
                cmdMap.put(argCmd.value().split(" ")[0], method);
            }
        }
    }
    
    private void showCommands(){
        System.out.println(cmdDocs);
    }
}

以上主要程式邏輯放在 exec 這個方法上,它會透過呼叫 initCommands 方法,建立所有選項常式之描述 (cmdDocs) 及對應 (cmdMap)。完成後,就是實際去解析命令列參數。如果沒有任何參數,就透過 showCommands 顯示說明。如果有,就進行選項與主程式方法的對應。