程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java編程的動態性,第3部分: 應用反射

Java編程的動態性,第3部分: 應用反射

編輯:關於JAVA

命令行參數處理是一項令人厭煩的零碎工作,不管您過去已經處理過多少次了,它好像總 能重新擺在您的面前。與其一遍又一遍地編寫同一塊代碼的不同變種,為什麼不利用反射來 簡化參數處理的工作呢?Java 顧問 Dennis Sosnoski 向您展示了如何做到這一點。在本文 中,Dennis 簡明扼要地介紹了一個開源庫,這個庫可以使得命令行參數實際上自己處理自己 。

在 上個月的文章中,我介紹了Java Reflection API,並簡要地講述了它的一些基本功能 。我還仔細研究了反射的性能,並且在文章的最後給出了一些指導方針,告訴讀者在一個應 用程序中何時應該使用反射,何時不應該使用反射。在本月這一期的文章中,我將通過查看 一個應用程序來更深入地討論這一問題,這個應用程序是用於命令行參數處理的一個庫,它 能夠很好地體現反射的強項和弱點。

一開始,在真正進入編寫實現代碼的工作之前,我將首先定義要解決的問題,然後為這個 庫設計一個接口。不過,在開發這個庫的時候,我並不是按照上述步驟進行的――我先是盡 力簡化一群有公共代碼基礎的應用程序中的現有代碼,然後使之通用化。本文中使用的“定 義-設計-構建”這種線性序列比起完完整整地描述開發過程要簡練得多,而且,按照這種方 式來組織對開發過程的描述,我可以修正我原先的一些假設,並清理掉這個庫的代碼中一些 不必要的方面。您完全有希望發現將上述方式作為開發您自己的基於反射的應用程序時所使 用的模型十分管用。

定義問題

我曾經寫過許多使用命令行參數的Java應用程序。一開始,大多數應用程序都很小,但最 後有些應用程序卻變得大到出乎我的意料。下面是我觀察到的這些應用程序的變大過程的標 准模式:

一開始只有一個或者兩個參數,按照某種特定的順序排列。

考慮到這個應用程序有更多的事情要做,於是添加更多的參數。

厭倦了每次都輸入所有的參數,於是讓一些參數成為可選的參數,讓這些參數帶有默認的 值。

忘記了參數的順序,於是修改代碼,允許參數以任何順序排列。

將這個應用程序交給其他感興趣的人。但是他們並不知道這些參數各自代表什麼,於是又 為這些參數添加更完善的錯誤檢查和“幫助”描述。

當我進入到第5步的時候,我通常會後悔沒有將整個過程都放在第一步來做。好在我很快 就會忘記後面的那些階段,不到一兩個星期,我又會考慮另外一個簡單的小命令行程序,我 想擁有這個應用程序。有了這個想法之後,上述整個惡心的循環過程的重現只是時間的問題 。

有一些庫可以用來幫助進行命令行參數處理。不過,在本文中我會忽略掉這些庫,而是自 己動手創建一個庫。這不是(或者不僅僅是)因為我有著“非此處發明(not invented here )”的態度(即不願意用外人發明的東西,譯者注),而是因為想拿參數處理作為一個實例 。這樣一來,反射的強項和弱點便正好體現了對參數處理庫的需求。特別地,參數處理庫:

需要一個靈活的接口,用以支持各種應用程序。

對於每個應用程序,都必須易於配置。

不要求頂級的性能,因為參數只需處理一次。

不存在訪問安全性問題,因為命令行應用程序運行的時候通常不帶安全管理器。

這個庫中實際的反射代碼只代表整個實現的一小部分,因此我將主要關注與反射最相關的 一些方面。

草擬出一份設計

應用程序訪問參數數據最方便的方式或許是通過該應用程序的 main 對象的一些字段。例 如,假設您正在編寫一個用於生成業務計劃的應用程序。您可能想使用一個 boolean 標記來 控制業務計劃是簡要的還是冗長的,使用一個 int 作為第一年的收入,使用一個 String 作 為對產品的描述。我將把這些會影響應用程序的運行的變量稱作 形參(parameters),以便 與命令行提供的 實參(arguments)――即形參的值區分開來。通過為這些形參使用字段, 將使得在需要形參的應用程序代碼中的任何地方都可以方便地調用它們。而且,如果使用字 段的話,在定義形參字段時為任意形參設置默認值也很方便,如清單1所示:

清單 1.業務計劃生成器(部分清單)

public class PlanGen {
   private boolean m_isConcise;     // rarely used, default  false
   private int m_initialRevenue = 1000; // thousands, default is  1M
   private float m_growthRate = 1.5;   // default is 50% growth  rate
   private String m_productDescription = // McD look out, here I  come
     "eFood - (Really) Fast Food Online";
   ...
   private int revenueForYear(int year) {
     return (int)(m_initialRevenue * Math.pow(m_growthRate, year- 1));
   }
   ...

反射將使得應用程序可以直接訪問這些私有字段,允許參數處理庫在應用程序代碼中沒有 任何特殊鉤子的情況下設置參數的值。但是我 的確需要某種方法能讓這個庫將這些字段與特 定的命令行參數相關起來。在我能夠定義一個參數和一個字段之間的這種關聯如何與庫進行 通信之前,我需要決定我希望如何格式化這些命令行參數。

對於本文,我將定義一種命令行格式,這是UNIX慣例的一種簡化版本。形參的實參值可以 以任何順序提供,在最前面使用一個連字符以指示一個實參給出了一個或者多個單字符的形 參標記(與實際的形參的值相對)。對於這個業務計劃生成器,我將采用以下形參標記字符 :

c -- 簡要計劃

f -- 第一年收入(千美元)

g -- 增長率(每年)

n -- 產品名稱

boolean 形參只需標記字符本身就可以設置一個值,而其他類型的形參還需要某種附加的 實參信息。對於數值實參,我只將它的值緊跟在形參標記字符之後(這意味著數字不能用作 標記字符),而對於帶 String 類型值的形參,我將在命令行中使用跟在標記字符後面的實 參作為實際的值。最後,如果還需要一些形參(例如業務計劃生成器的輸出文件的文件名) ,我假設這些形參的實參值跟在命令行中可選形參值的後面。有了上面給出的這些約定,業 務計劃生成器的命令行看上去就是這個樣子:

java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt

如果把它放在一起,那麼每個實參的意思就是:

-c -- 生成簡要計劃

-f2500 -- 第一年收入為 $2,500,000

-g2.5 -- 每年增長率為250%

-n "iSue4U . . ." -- 產品名稱是 "iSue4U . . ."

plan.txt -- 需要的輸出文件名

這時,我已經得到了參數處理庫的基本功能的規范說明書。下一步就是為這個應用代碼定 義一個特定的接口,以使用這個庫。

選擇接口

您可以使用單個的調用來負責命令行參數的實際處理,但是這個應用程序首先需要以某種 方式將它的特定的形參定義到庫中。這些形參可以具有不同的幾種類型(對於業務計劃生成 器的例子,形參的類型可以是 boolean , int、float 和 java.lang.String )。每種類型 可能又有一些特殊的需求。例如,如果給出了標記字符的話,將 boolean 形參定義為 false 會比較好,而不是總將它定義為 true 。而且,為一個 int 值定義一個有效范圍也很有用。

我處理這些不同需求的方法是,首先為所有形參定義使用一個基類,然後為每一種特定類 型的形參細分類這個基類。這種方法使得應用程序可以以基本形參定義類的實例數組的形式 將形參定義提供給這個庫,而實際的定義則可以使用匹配每種形參類型的子類。對於業務計 劃生成器的例子,這可以采用清單2中所示的形式:

清單 2. 業務計劃生成器的形參定義

private static final ParameterDef[] PARM_DEFS = {
   new BoolDef('c', "m_isConcise"),
   new IntDef('f', "m_initialRevenue", 10, 10000),
   new FloatDef('g', "m_growthRate", 1.0, 100.0),
   new StringDef('n', "m_productDescription")
}

有了得到允許的在一個數組中定義的形參,應用程序對參數處理代碼的調用就可以像對一 個靜態方法的單個調用一樣簡單。為了允許除形參數組中定義的實參之外額外的實參(要麼 是必需的值,要麼是可變長度的值),我將令這個調用返回被處理實參的實際數量。這樣應 用程序便可以檢查額外的實參並適當地使用它們。最後的結果看上去如清單3所示:

清單 3. 使用庫

public class PlanGen
{
   private static final ParameterDef[] PARM_DEFS = {
     ...
   };

   public static void main(String[] args) {

     // if no arguments are supplied, assume help is needed
     if (args.length > 0) {

       // process arguments directly to instance
       PlanGen inst = new PlanGen();
       int next = ArgumentProcessor.processArgs
         (args, PARM_DEFS, inst);

       // next unused argument is output file name
       if (next >= args.length) {
         System.err.println("Missing required output file  name");
         System.exit(1);
       }
       File outf = new File(args[next++]);
       ...
     } else {
       System.out.println("\nUsage: java PlanGen " +
       "[-options] file\nOptions are:\n c concise plan\n" +
       "f first year revenue (K$)\n g growth rate\n" +
       "n product description");
     }
   }
}

最後剩下的部分就是處理錯誤報告(例如一個未知的形參標記字符或者一個超出范圍的數 字值)。出於這個目的,我將定義 ArgumentErrorException 作為一個未經檢查的異常,如 果出現了某個這一類的錯誤,就將拋出這個異常。如果這個異常沒有被捕捉到,它將立即關 閉應用程序,並將一條錯誤消息和棧跟蹤輸出到控制台。一個替代的方法是,您也可以在代 碼中直接捕捉這個異常,並且用其他方式處理異常(例如,可能會與使用信息一起輸出真正 的錯誤消息)。

實現庫

為了讓這個庫像計劃的那樣使用反射,它需要查找由形參定義數組指定的一些字段,然後 將適當的值存到這些來自相應的命令行參數的字段中。這項任務可以通過只查找實際的命令 行參數所需的字段信息來處理,但是我反而選擇將查找和使用分開。我將預先找到所有的字 段,然後只使用在參數處理期間已經被找到的信息。

預先找到所有的字段是一種防錯性編程的步驟,這樣做可以消除使用反射時帶來的一個潛 在的問題。如果我只是查找需要的字段,那麼就很容易破壞一個形參定義(例如,輸錯相應 的字段名),而且還不能認識到有錯誤發生。這裡不會有編譯時錯誤,因為字段名是作為 String 傳遞的,而且,只要命令行沒有指定與已破壞的形參定義相匹配的實參,程序也可以 執行得很好。這種被蒙蔽的錯誤很容易導致不完善代碼的發布。

假設我想在實際處理實參之前查找字段信息,清單4顯示了用於形參定義的基類的實現, 這個實現帶有一個 bindToClass() 方法,用於處理字段查找。

清單 4. 用於形參定義的基類

public abstract class ParameterDef
{
   protected char m_char;     // argument flag character
   protected String m_name;    // parameter field name
   protected Field m_field;    // actual parameter field

   protected ParameterDef(char chr, String name) {
     m_char = chr;
     m_name = name;
   }
   public char getFlag() {
     return m_char;
   }
   protected void bindToClass(Class clas) {
     try {

       // handle the field look up and accessibility
       m_field = clas.getDeclaredField(m_name);
       m_field.setAccessible(true);

     } catch (NoSuchFieldException ex) {
       throw new IllegalArgumentException("Field '" +
         m_name + "' not found in " + clas.getName());
     }
   }
   public abstract void handle(ArgumentProcessor proc);
}

實際的庫實現還涉及到本文沒有提及的幾個類。我不打算一一介紹每一個類,因為其中大 部分類都與庫的反射方面不相關。我將提到的是,我選擇將目標對象存為 ArgumentProcessor 類的一個字段,並在這個類中實現一個形參字段的真正設置。這種方法 為參數處理提供了一個簡單的模式: ArgumentProcessor 類掃描實參以發現形參標記,為每 個標記查找相應的形參定義(總是 ParameterDef 的一個子類),再調用這個定義的 handle() 方法。handle() 方法在解釋完實參值之後,又調用 ArgumentProcessor 的 setValue() 方法。清單5顯示了 ArgumentProcessor 類的不完整版本,包括在構造函數中的 形參綁定調用以及 setValue() 方法:

清單 5. 主庫類的部分清單

public class ArgumentProcessor
{
   private Object m_targetObject; // parameter value object
   private int m_currentIndex;   // current argument position
   ...
   public ArgumentProcessor(ParameterDef[] parms, Object target)  {

     // bind all parameters to target class
     for (int i = 0; i < parms.length; i++) {
       parms[i].bindToClass(target.getClass());
     }

     // save target object for later use
     m_targetObject = target;
   }

   public void setValue(Object value, Field field) {
     try {

       // set parameter field value using reflection
       field.set(m_targetObject, value);

     } catch (IllegalAccessException ex) {
       throw new IllegalArgumentException("Field " + field.getName()  +
         " is not accessible in object of class " +
         m_targetObject.getClass().getName());
     }
  }

   public void reportArgumentError(char flag, String text) {
    throw new ArgumentErrorException(text + " for argument '" +
     flag + "' in argument " + m_currentIndex);
   }

   public static int processArgs(String[] args,
     ParameterDef[] parms, Object target) {
     ArgumentProcessor inst = new ArgumentProcessor(parms,  target);
     ...
   }
}

最後,清單6顯示了 int 形參值的形參定義子類的部分實現。這包括對基類的 bindToClass() 方法(來自 清單4)的重載,這個重載的方法首先調用基類的實現,然後檢 查找到的字段是否匹配預期的類型。其他特定形參類型( boolean、float、String ,等等 )的子類與此十分相似。

清單 6. int 形參定義類

public class IntDef extends ParameterDef
{
   private int m_min;       // minimum allowed value
   private int m_max;       // maximum allowed value

   public IntDef(char chr, String name, int min, int max) {
     super(chr, name);
     m_min = min;
     m_max = max;
   }
   protected void bindToClass(Class clas) {
     super.bindToClass(clas);
     Class type = m_field.getType();
     if (type != Integer.class && type != Integer.TYPE)  {
       throw new IllegalArgumentException("Field '" + m_name +
         "'in " + clas.getName() + " is not of type int");
     }
   }
   public void handle(ArgumentProcessor proc) {

     // set up for validating 
     boolean minus = false;
     boolean digits = false;
     int value = 0;

     // convert number supplied in argument list to 'value'
     ...

     // make sure we have a valid value
     value = minus ? -value : value;
     if (!digits) {
       proc.reportArgumentError(m_char, "Missing value");
     } else if (value < m_min || value > m_max) {
       proc.reportArgumentError(m_char, "Value out of range");
     } else {
       proc.setValue(new Integer(value), m_field);
     }
   }
}

結束庫

在本文中,我講述了一個用於處理命令行參數的庫的設計過程,作為反射的一個實際的例 子。這個庫很好地闡明了如何有效地使用反射――它簡化應用程序的代碼,而且不用明顯地 犧牲性能。犧牲了多少性能呢?從對我的開發系統的一些快速測試中可以看出,一個簡單的 測試程序在使用整個庫進行了參數處理時比起不帶任何參數處理時運行起來平均只慢40毫秒 。多出來的這些時間大部分是花在庫類和庫代碼所使用的其他類的裝載上,因此,即使是對 於那些定義了許多命令行形參和許多實參值的應用程序,也不大可能會比這一結果糟很多。 對於我的命令行應用程序,額外的40毫秒根本不能引起我的注意。

它包括我在本文沒有提到的一些特性,包括這樣一些細節,比如鉤子,用於容易地生成一 列格式化的形參標記,還有一些描述,有助於為應用程序提供使用指令。歡迎您在自己的程 序中使用這個庫,並以任何您發現有用的方式擴展這個庫。

現在我已講過了 第1部分中Java類的基礎,也講過了 第2部分中的 Java Reflection API 的原理以及第3部分,本系列剩下的部分將改變話題,講講大家不大熟悉的字節碼處理。在第 4部分,我將從容易的開始,先看看用於使用二進制類的用戶友好的 Javassist 庫。您是否 想轉換方法,但是又不願在字節碼中啟動程序呢?Javassist 正好適合您的需求。下個月我 們將看看如何實現這一點。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved