Java 編程的動態性:應用反射

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

  1. 一開始只有一個或者兩個參數,按照某種特定的順序排列。
  2. 考慮到這個應用程序有更多的事情要作,因而添加更多的參數。
  3. 厭倦了每次都輸入全部的參數,因而讓一些參數成爲可選的參數,讓這些參數帶有默認的值。
  4. 忘記了參數的順序,因而修改代碼,容許參數以任何順序排列。
  5. 將這個應用程序交給其餘感興趣的人。可是他們並不知道這些參數各自表明什麼,因而又爲這些參數添加更完善的錯誤檢查和「幫助」描述。

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

有一些庫能夠用來幫助進行命令行參數處理。不過,在本文中我會忽略掉這些庫,而是本身動手建立一個庫。這不是(或者不只僅是)由於我有着「非此處發明(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 測試

若是把它放在一塊兒,那麼每一個實參的意思就是: ui

  • -c-- 生成簡要計劃
  • -f2500-- 第一年收入爲 $2,500,000
  • -g2.5-- 每一年增加率爲250%
  • -n "iSue4U . . ."-- 產品名稱是 "iSue4U . . ."
  • plan.txt-- 須要的輸出文件名

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

選擇接口

您可使用單個的調用來負責命令行參數的實際處理,可是這個應用程序首先須要以某種方式將它的特定的形參定義到庫中。這些形參能夠具備不一樣的幾種類型(對於業務計劃生成器的例子,形參的類型能夠是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毫秒根本不能引發個人注意。

相關文章
相關標籤/搜索