其實在 Web 上,同樣的問題也存在,就是怎麼把 URL 對應到特定的 servlet, action 或 controller 的方法上。這不管是在 Spring Framework 3 Web MVC, Servlet 3 或是 Jessey 中,都有一套近似的解法,就是使用 @Annotation。
OK, 那我們何不如法泡製,也用 @Annotation 來處理命令列參數呢? 我設計的命令列規範如下:
- 將命令列參數區分成「選項」及「選項參數」。選項如 -start, -stop;選項參數如 -start localhost 18080 中,未加 dash 符號的字串,即紅字部分。
- 一個命令列可以接受多個選項及多個選項參數。
- 支援將選項綁定到主程式裡面的方法,以下我稱之為「選項常式」。
- 支援將選項參數綁定到選項常式的參數上。
- 支援自動列出所有選項及其說明。
- 自動檢查選項參數是否正確。
- 支選整數型態的選項參數。
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 顯示說明。如果有,就進行選項與主程式方法的對應。
沒有留言:
張貼留言