Java動態代理學習文章(二)

原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-proxy2/index.html

本文希望將 Java 動態代理機制從接口擴展到類,使得類能夠享有與接口類似的動態代理支持。

設計及特點

新擴展的類名爲 ProxyEx,將直接繼承於 java.lang.reflect.Proxy,也聲明瞭與原 Proxy 類中同名的 public 靜態方法,目的是保持與原代理機制在使用方法上的完全一致。


圖 1. ProxyEx 類繼承圖
圖 1. ProxyEx 類繼承圖 

與原代理機制最大的區別在於,動態生成的代理類將不再從 Proxy 類繼承,改而繼承需被代理的類。由於 Java 的單繼承原則,擴展代理機制所支持的類數目不得多於一個,但它可以聲明實現若干接口。包管理的機制與原來相似,不支持一個以上的類和接口同時爲非 public;如果僅有一個非 public 的類或接口,假設其包爲 PackageA,則動態生成的代理類將位於包 PackageA;否則將位於被代理的類所在的包。生成的代理類也被賦予 final 和 public 訪問屬性,且其命名規則類似地爲“父類名 +ProxyN”(N 也是遞增的阿拉伯數字)。最後,在異常處理方面則與原來保持完全一致。


圖 2. 動態生成的代理類的繼承圖
圖 2. 動態生成的代理類的繼承圖 

模板

通過對 Java 動態代理機制的推演,我們已經獲得了一個通用的方法模板。可以預期的是,通過模板來定製和引導代理類的代碼生成,是比較可行的方法。我們將主要使用兩個模板:類模板和方法模板。


清單 1. 類模板
				
package &Package;
final public class &Name &Extends &Implements
{
    private java.lang.reflect.InvocationHandler handler = null;
    &Constructors
    &Methods
} 

類模板定製了代理類的代碼框架。其中帶“&”前綴的標籤位被用來引導相應的代碼替換。在此預留了包(&Package)、類名(&ClassName)、類繼承(&Extends)、接口實現(&Implements)、構造函數集(&Constructors)及方法集(&Methods)的標籤位。類模板還同時聲明瞭一個私有型的調用處理器對象作爲類成員。


清單 2. 方法模板
				
&Modifiers &ReturnType &MethodName(&Parameters) &Throwables
{    
   java.lang.reflect.Method method = null;
   try {
        method = &Class.getMethod( \"& MethodName\", &ParameterTypes );
    }
    catch(Exception e){
    }
    Object r = null;
    try{
        r = handler.invoke( this, method, &ParameterValues );
    }&Exceptions
    &Return
}

方法模板定製了代理類方法集合中各個方法的代碼框架,同樣的帶“&”前綴的標籤位被用來引導相應的代碼替換。在此預留了修飾符(&Modifiers)、返回類型(&ReturnType)、方法名(&MethodName)、參數列表(Parameters)、異常列表(&Throwables)、方法的聲明類(&Class)、參數類型列表(&ParameterTypes)、調用處理器的參數值列表(&ParameterValues),異常處理(&Exceptions)及返回值(&Return)的標籤位。

代碼生成

有了類模板和方法模板,代碼生成過程就變得有章可依。基本過程可分爲三步:1)生成代理類的方法集合;2)生成代理類的構造函數;3)最後生成整個代理類。

生成代理類的方法集

第一步,通過反射獲得被代理類的所有 public 或 protected 且非 static 的 Method 對象列表,這些方法將被涵蓋的原因是它們是可以被其他類所訪問的。

第二步,遍歷 Method 對象列表,對每個 Method 對象,進行相應的代碼生成工作。


清單 3. 對標籤位進行代碼替換生成方法代碼
				
String declTemplate = "&Modifiers &ReturnType &MethodName(&Parameters) &Throwables";
String bodyTemplate = "&Declaration &Body";
// 方法聲明
String declare = declTemplate.replaceAll("&Modifiers", getMethodModifiers( method ))
    .replaceAll("&ReturnType", getMethodReturnType( method ))
    .replaceAll("&MethodName", method.getName())
    .replaceAll("&Parameters", getMethodParameters( method ))
    .replaceAll("&Throwables", getMethodThrowables( method ));

// 方法聲明以及實現
String body = bodyTemplate.replaceAll("&Declaration", declare )
    .replaceAll("&Body", getMethodEntity( method ));

這裏涉及了一些 ProxyEx 類的私有的輔助函數如 getMethodModifiers 和 getMethodReturnType 等等,它們都是通過反射獲取所需的信息,然後動態地生成各部分代碼。函數 getMethodEntity 是比較重要的輔助函數,它又調用了其他的輔助函數來生成代碼並替換標籤位。


清單 4. ProxyEx 的靜態方法 getMethodEntity()
			
private static String getMethodEntity( Method method )
{
    String template =  "\n{"
        + "\n    java.lang.reflect.Method method = null;"
        + "\n    try{"
        + "\n        method = &Class.getMethod( \"&MethodName\", &ParameterTypes );"
        + "\n    }"
        + "\n    catch(Exception e){"
        + "\n    }"
        + "\n    Object r = null;"
        + "\n    try{" 
        + "\n         r = handler.invoke( this, method, &ParameterValues );"
        + "\n    }&Exceptions"
        + "\n    &Return"
        + "\n}";
    
    String result = template.replaceAll("&MethodName", method.getName() )
        .replaceAll("&Class", method.getDeclaringClass().getName() + ".class")
        .replaceAll("&ParameterTypes",  getMethodParameterTypesHelper(method))
        .replaceAll("&ParameterValues",  getMethodParameterValuesHelper(method) )
        .replaceAll("&Exceptions", getMethodParameterThrowablesHelper(method))
        .replaceAll("&Return", getMethodReturnHelper( method ) );
    
    return result;
}

當爲 Class 類型對象生成該類型對應的字符代碼時,可能涉及數組類型,反推過程會需要按遞歸方法生成代碼,這部分工作由 getTypeHelper 方法提供


清單 5. ProxyEx 的靜態方法 getTypeHelper()
				
private static String getTypeHelper(Class type)
{
    if( type.isArray() )
    {
        Class c = type.getComponentType();
        return getTypeHelper(c) + "[]";
    }
    else
    {
        return type.getName();
    }
}

第三步,將所生成的方法保存進一個 map 表,該表記錄的是鍵值對(方法聲明,方法實現)。由於類的多態性,父類的方法可能被子類所覆蓋,這時以上通過遍歷所得的方法列表中就會出現重複的方法對象,維護該表可以很自然地達到避免方法重複生成的目的,這就維護該表的原因所在。

生成代理類的構造函數

相信讀者依然清晰記得代理類是通過其構造函數反射生成的,而構造時傳入的唯一參數就是調用處理器對象。爲了保持與原代理機制的一致性,新的代理類的構造函數也同樣只有一個調用處理器對象作爲參數。模板簡單如下


清單 6. 構造函數模板
				

public &Constructor(java.lang.reflect.InvocationHandler handler) 


    super(&Parameters); 

    this.handler = handler; 

} 


需要特別提一下的是 super 方法的參數值列表 &Parameters 的生成,我們借鑑了 Mock 思想,側重於追求對象構造的成功,而並未過多地努力分析並尋求最準確最有意義的

值。對此,相信讀者會多少產生一些疑慮,但稍後我們會提及改進的方法,請先繼續閱讀。

生成整個代理類

通過以上步驟,構造函數和所有需被代理的方法的代碼已經生成,接下來就是生成整個代理類的時候了。這個過程也很直觀,通過獲取相關信息並對類模板中各個標籤位進行替換,便可以輕鬆的完成整個代理類的代碼生成。

被遺忘的角落:類變量

等等,似乎遺忘了什麼?從調用者的角度出發,我們希望代理類能夠作爲被代理類的如實代表呈現在用戶面前,包括其內部狀態,而這些狀態通常是由類變量所體現出來的,於是就涉及到類變量的代理問題。

要解決這個問題,首先需要思考何時兩者的類變量可能出現不一致?回答了這個問題,也就找到了解決思路。回顧代理類的構造函數,我們以粗糙的方式構造了代理類實例。它們可能一開始就已經不一致了。還有每次方法調用也可能導致被兩者的類變量的不一致。如何解決?直觀的想法是:1)構造時需設法進行同步;2)方法調用之前和之後也需設法進行同步。這樣,我們就能夠有效避免代理類和被代理類的類變量不一致的問題的出現了。

但是,如何獲得被代理類的實例呢?從當前的的設計中已經沒有辦法做到。既然如此,那就繼續我們的擴展之旅。只不過這次擴展的對象是調用處理器接口,我們將在擴展後的接口裏加入獲取被代理類對象的方法,且擴展調用處理器接口將以 static 和 public 的形式被定義在 ProxyEx 類中。


清單 7. ProxyEx 類內的靜態接口 InvocationHandlerEx
				

public static interface InvocationHandlerEx extends InvocationHandler 


    // 返回指定 stubClass 參數所對應的被代理類實體對象

    Object getStub(Class stubClass); 

} 


新的調用處理器接口具備了獲取被代理類對象的能力,從而爲實現類變量的同步打開了通道。接下來還需要的就是執行類變量同步的 sync 方法,每個動態生成的代理類中都會被悄悄地加入這個私有方法以供調用。每次方法被分派轉發到調用處理器執行之前和之後,sync 方法都會被調用,從而保證類變量的雙向實時更新。相應的,方法模板也需要更新以支持該新特性。


清單 8. 更新後的方法模板(部分)
				

Object r = null;

try{

    // 代理類到被代理類方向的變量同步

    sync(&Class, true);

    r = handler.invoke( this, method, &ParameterValues );

    // 被代理類到代理類方向的變量同步

    sync(&Class, false);

}&Exceptions


&Return

sync 方法還會在構造函數尾部被調用,從而將被代理類對象的變量信息同步到代理類對象,實現類似於拷貝構造的等價效果。相應的,構造函數模板也需要更新以支持該新特性。


清單 9. 更新後的構造函數模板
				

public &Name(java.lang.reflect.InvocationHandler handler)

{

    super(&Parameters);

    this.handler = handler;

    // 被代理類到代理類方向的變量同步

    sync(null, false);

}

接下來介紹 sync 方法的實現,其思想就是首先獲取被代理類的所有 Field 對象的列表,並通過擴展的調用處理器獲得方法的聲明類說對應的 stub 對象,然後遍歷 Field 對象列表並對各個變量進行拷貝同步。


清單 10. 聲明在動態生成的代理類內部的 snyc 函數
				

private synchronized void sync(java.lang.Class clazz, boolean toStub)

{

    // 判斷是否爲擴展調用處理器

    if( handler instanceof InvocationHandlerEx )

    {

        java.lang.Class superClass = this.getClass().getSuperclass();

        java.lang.Class stubClass = ( clazz != null ? clazz : superClass );

       

        // 通過擴展調用處理器獲得stub對象

        Object stub = ((InvocationHandlerEx)handler).getStub(stubClass);

        if( stub != null )

        {

            // 獲得所有需同步的類成員列表,遍歷並同步

            java.lang.reflect.Field[] fields = getFields(superClass);

            for(int i=0; fields!=null&&i<fields.length; i++)

            {

                try

                {

                    fields[i].setAccessible(true);

                    // 執行代理類和被代理類的變量同步

                    if(toStub)

                    {

                        fields[i].set(stub, fields[i].get(this));

                    }

                    else

                    {

                        fields[i].set(this, fields[i].get(stub));

                    }

                }

                catch(Throwable e)

                {

                }

            }

        }

    }

}


這裏涉及到一個用於獲取類的所有 Field 對象列表的靜態輔助方法 getFields。爲了提高頻繁查詢時的性能,配合該靜態方法的是一個靜態的 fieldsMap 對象,用於記錄已查詢過的類其所包含的 Field 對象列表,使得再次查詢時能迅速返回其對應列表。相應的,類模板也需進行更新。


清單 11. 增加了靜態 fieldsMap 變量後的類模板
				

package &Package;

final public class &Name &Extends &Implements

{

    private static java.util.HashMap fieldsMap = new java.util.HashMap();

    private java.lang.reflect.InvocationHandler handler = null;

    &Constructors

    &Methods

}



清單 12. 聲明在動態生成的代理類內部的靜態方法 getFields
				

private static java.lang.reflect.Field[] getFields(java.lang.Class c)

{

    if( fieldsMap.containsKey(c) )

    {

        return (java.lang.reflect.Field[])fieldsMap.get(c);

    }

    

    java.lang.reflect.Field[] fields = null;

    if( c == java.lang.Object.class )

    {

        fields = c.getDeclaredFields();

    }

    else

    {

        java.lang.reflect.Field[] fields0 = getFields(c.getSuperclass());

        java.lang.reflect.Field[] fields1 = c.getDeclaredFields();

        fields = new java.lang.reflect.Field[fields0.length + fields1.length];

        System.arraycopy(fields0, 0, fields, 0, fields0.length);

        System.arraycopy(fields1, 0, fields, fields0.length, fields1.length);    }

    fieldsMap.put(c, fields);

    return fields;


}


動態編譯及裝載

代碼生成以後,需要經過編譯生成 JVM 所能識別的字節碼,而字節碼還需要通過類裝載器載入 JVM 才能最終被真正使用,接下來我們將闡述如何動態編譯及裝載。

首先是動態編譯。這部分由 ProxyEx 類的 getProxyClassCodeSource 函數完成。該函數分三步進行:第一步保存源代碼到 .java 文件;第二步編譯該 .java 文件;第三步從輸出的 .class 文件讀取字節碼。


清單 13. ProxyEx 的靜態方法 getProxyClassCodeSource
				

private static byte[] getProxyClassCodeSource( String pkg, String className, 

    String declare ) throws Exception

{

    // 將類的源代碼保存進一個名爲類名加“.java”的本地文件

    File source = new File(className + ".java");

    FileOutputStream fos = new FileOutputStream( source );

    fos.write( declare.getBytes() );

    fos.close();

    

    // 調用com.sun.tools.javac.Main類的靜態方法compile進行動態編譯

    int status = com.sun.tools.javac.Main.compile( new String[] { 

        "-d", 

        ".", 

        source.getName() } );

    if( status != 0 )


    {

        source.delete();        

        throw new Exception("Compiler exit on " + status);

    }


    

    // 編譯得到的字節碼將被輸出到與包結構相同的一個本地目錄,文件名爲類名加”.class”

    String output = ".";

    int curIndex = -1;

    int lastIndex = 0;

    while( (curIndex=pkg.indexOf('.', lastIndex)) != -1 )

    {

        output = output + File.separator + pkg.substring( lastIndex, curIndex );

        lastIndex = curIndex + 1;

    }


    output = output + File.separator + pkg.substring( lastIndex );

    output = output + File.separator + className + ".class";

    

    // 從輸出文件中讀取字節碼,並存入字節數組

    File target = new File(output);

    FileInputStream f = new FileInputStream( target );

    byte[] codeSource = new byte[(int)target.length()];

    f.read( codeSource );

    f.close();

    

    // 刪除臨時文件

    source.delete();

    target.delete();

    


    return codeSource;}


得到代理類的字節碼,接下來就可以動態裝載該類了。這部分由 ProxyEx 類的 defineClassHelper 函數完成。該函數分兩步進行:第一步通過反射獲取父類 Proxy 的靜態私有方法 defineClass0;第二步傳入字節碼數組及其他相關信息並反射調用該方法以完成類的動態裝載。


清單 14. ProxyEx 的靜態方法 defineClassHelper
				

private static Class defineClassHelper( String pkg, String cName, byte[] codeSource ) 

    throws Exception

{

    Method defineClass = Proxy.class.getDeclaredMethod( "defineClass0", 

        new Class[] { ClassLoader.class, 

            String.class,

            byte[].class,

            int.class,

            int.class } );


    defineClass.setAccessible(true);

    return (Class)defineClass.invoke( Proxy.class, 

        new Object[] { ProxyEx.class.getClassLoader(), 

        pkg.length()==0 ? cName : pkg+"."+cName,

        codeSource,

        new Integer(0),

        new Integer(codeSource.length) } );

}

性能改進

原動態代理機制中對接口數組有一些有趣的特點,其中之一就是接口的順序差異會在一定程度上導致生成新的代理類,即使其實並無必要。其中的原因就是因爲緩存表是以接口名稱列表作爲關鍵字,所以不同的順序就意味着不同的關鍵字,如果對應的關鍵字不存在,就會生成新但是作用重複的代理類。在 ProxyEx 類中,我們通過主動排序避免了類似的問題,提高動態生成代理類的效率。而且,如果發現數組中都是接口類型,則直接調用父類 Proxy 的靜態方法 getProxyClass 生成代理類,否則才通過擴展動態代理機制生成代理類,這樣也一定程度上改進了性能。

兼容性問題

接下來需要考慮的是與原代理機制的兼容性問題。曾記否,Proxy 中還有兩個靜態方法:isProxyClass 和 getInvocationHandler,分別被用於判斷 Class 對象是否是動態代理類和從 Object 對象獲取對應的調用處理器(如果可能的話)。


清單 15. Proxy 的靜態方法 isProxyClass 和 getInvocationHandler
				

static boolean isProxyClass(Class cl) 

static InvocationHandler getInvocationHandler(Object proxy) 


現在的兼容性問題,主要涉及到 ProxyEx 類與父類 Proxy 在關於動態生成的代理類的信息方面所面臨的如何保持同步的問題。曾介紹過,在 Proxy 類中有個私有的 Map 對象 proxyClasses 專門負責保存所有動態生成的代理類類型。Proxy 類的靜態函數 isProxyClass 就是通過查詢該表以確定某 Class 對象是否爲動態代理類,我們需要做的就是把由 ProxyEx 生成的代理類類型也保存入該表。這部分工作由 ProxyEx 類的靜態方法 addProxyClass 輔助完成。


清單 16. ProxyEx 的靜態方法 addProxyClass
				

private static void addProxyClass( Class proxy ) throws IllegalArgumentException 

{ 

    try 

    { 

        // 通過反射獲取父類的私有 proxyClasses 變量並更新

        Field proxyClasses = Proxy.class.getDeclaredField("proxyClasses"); 

        proxyClasses.setAccessible(true); 

        ((Map)proxyClasses.get(Proxy.class)).put( proxy, null ); 

    } 

    catch(Exception e) 

    { 

        throw new IllegalArgumentException(e.toString()); 

    } 

} 


相對而言,原來 Proxy 類的靜態方法 getInvocationHandler 實現相當簡單,先判斷是否爲代理類,若是則直接類型轉換到 Proxy 並返回其調用處理器成員,而擴展後的代理類並不非從 Proxy 類繼承,所以在獲取調用處理器對象的方法上需要一些調整。這部分由 ProxyEx 類的同名靜態方法 getInvocationHandler 完成。


清單 17. ProxyEx 的靜態方法 getInvocationHandler
				

public static InvocationHandler getInvocationHandler(Object proxy) 

    throws IllegalArgumentException

{

    // 如果Proxy實例,直接調父類的方法

    if( proxy instanceof Proxy )

{

        return Proxy.getInvocationHandler( proxy );

    }

    

    // 如果不是代理類,拋異常

    if( !Proxy.isProxyClass( proxy.getClass() ))

    {

        throw new IllegalArgumentException("Not a proxy instance");

    }

        

    try

{

        // 通過反射獲取擴展代理類的調用處理器對象

        Field invoker = proxy.getClass().getDeclaredField("handler");

        invoker.setAccessible(true);

        return (InvocationHandler)invoker.get(proxy);

    }

    catch(Exception e)

    {

        throw new IllegalArgumentException("Suspect not a proxy instance");

    }


}

坦言:也有侷限

受限於 Java 的類繼承機制,擴展的動態代理機制也有其侷限,它不能支持:

  1. 聲明爲 final 的類;
  2. 聲明爲 final 的函數;
  3. 構造函數均爲 private 類型的類;

實例演示

闡述了這麼多,相信讀者一定很想看一下擴展動態代理機制是如何工作的。本文最後將以 2010 世博門票售票代理爲模型進行演示。

首先,我們定義了一個售票員抽象類 TicketSeller。


清單 18. TicketSeller
				

public abstract class TicketSeller 

{

    protected String theme;

    protected TicketSeller(String theme)

    {

        this.theme = theme;

    }

    public String getTicketTheme()

    {

        return this.theme;

    }

    public void setTicketTheme(String theme)

    {

        this.theme = theme;

    }

    public abstract int getTicketPrice();

    public abstract int buy(int ticketNumber, int money) throws Exception;

}

其次,我們會實現一個 2010 世博門票售票代理類 Expo2010TicketSeller。


清單 19. Expo2010TicketSeller
				
public class Expo2010TicketSeller extends TicketSeller 

{

    protected int price;

    protected int numTicketForSale;

    public Expo2010TicketSeller()

    {

        super("World Expo 2010");

        this.price = 180;

        this.numTicketForSale = 200;

    }

    public int getTicketPrice()

    {

        return price;

    }

    public int buy(int ticketNumber, int money) throws Exception

    {

        if( ticketNumber > numTicketForSale )

        {

            throw new Exception("There is no enough ticket available for sale, only " 

                + numTicketForSale + " ticket(s) left");

        }

        int charge = money - ticketNumber * price;

        if( charge < 0 )

        {

            throw new Exception("Money is not enough. Still needs " 

	        + (-charge) + " RMB.");

        }

        numTicketForSale -= ticketNumber;

        return charge;

    }

}


接着,我們將通過購票者類 TicketBuyer 來模擬購票以演示擴展動態代理機制。


清單 20. TicketBuyer
				

public class TicketBuyer 

{

    public static void main(String[] args) 

    {

        // 創建真正的TickerSeller對象,作爲stub實體

        final TicketSeller stub = new Expo2010TicketSeller();


        // 創建擴展調用處理器對象

        InvocationHandler handler = new InvocationHandlerEx()

        {

            public Object getStub(Class stubClass) 

            {

                // 僅對可接受的Class類型返回stub實體

                if( stubClass.isAssignableFrom(stub.getClass()) )

                {

                    return stub;

                }

                return null;

            }


            public Object invoke(Object proxy, Method method, Object[] args) 

            throws Throwable 

            {

                Object o;

                try

                {

                    System.out.println("   >>> Enter method: " 

		        + method.getName() );

                    o = method.invoke(stub, args);

                }

                catch(InvocationTargetException e)

                {

                    throw e.getCause();

                }

                finally

                {

                    System.out.println("   <<< Exit method: " 

		        + method.getName() );

                }

                return o;

            }

        };

        

        // 通過ProxyEx構造動態代理

        TicketSeller seller = (TicketSeller)ProxyEx.newProxyInstance(

                TicketBuyer.class.getClassLoader(), 

                new Class[] {TicketSeller.class}, 

                handler);

        

        // 顯示代理類的類型

        System.out.println("Ticket Seller Class: " + seller.getClass() + "\n");

        // 直接訪問theme變量,驗證代理類變量在對象構造時同步的有效性

        System.out.println("Ticket Theme: " + seller.theme + "\n");

        // 函數訪問price信息

        System.out.println("Query Ticket Price...");

        System.out.println("Ticket Price: " + seller.getTicketPrice() + " RMB\n");

        // 模擬票務交易

        buyTicket(seller, 1, 200);

        buyTicket(seller, 1, 160);

        buyTicket(seller, 250, 30000);

        // 直接更新theme變量

        System.out.println("Updating Ticket Theme...\n");

        seller.theme = "World Expo 2010 in Shanghai";

        // 函數訪問theme信息,驗證擴展動態代理機制對變量同步的有效性

        System.out.println("Query Updated Ticket Theme...");

        System.out.println("Updated Ticket Theme: " + seller.getTicketTheme() + "\n");    

    }

    // 購票函數

    protected static void buyTicket(TicketSeller seller, int ticketNumber, int money)

    {

        try 

        {

            System.out.println("Transaction: Order " + ticketNumber + " ticket(s) with " 

                + money + " RMB");

            int charge = seller.buy(ticketNumber, money);

            System.out.println("Transaction: Succeed - Charge is " + charge + " RMB\n");

        } 

        catch (Exception e) 

        {

            System.out.println("Transaction: Fail - " + e.getMessage() + "\n");

        }    

    }

}


最後,見演示程序的執行結果。


清單 21. 執行輸出
				

Ticket Seller Class: class com.demo.proxy.test.TicketSellerProxy0


Ticket Theme: World Expo 2010


Query Ticket Price...

   >>> Enter method: getTicketPrice

   <<< Exit method: getTicketPrice

Ticket Price: 180 RMB


Transaction: Order 1 ticket(s) with 200 RMB

   >>> Enter method: buy

   <<< Exit method: buy

Transaction: Succeed - Charge is 20 RMB


Transaction: Order 1 ticket(s) with 160 RMB

   >>> Enter method: buy

   <<< Exit method: buy

Transaction: Fail - Money is not enough. Still needs 20 RMB.


Transaction: Order 250 ticket(s) with 30000 RMB

   >>> Enter method: buy

   <<< Exit method: buy

Transaction: Fail - There is no enough ticket available for sale, only 199 ticket(s) left


Updating Ticket Theme...


Query Updated Ticket Theme...

   >>> Enter method: getTicketTheme

   <<< Exit method: getTicketTheme

Updated Ticket Theme: World Expo 2010 in Shanghai


參考資料

作者簡介

王忠平,軟件工程師,目前在 IBM 上海中國系統技術實驗室任職。

何平,軟件工程師,目前在 IBM 上海中國系統技術實驗室任職。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章