程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java動態代理機制分析及擴展,第2部分

Java動態代理機制分析及擴展,第2部分

編輯:關於JAVA

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

設計及特點

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

圖 1. ProxyEx 類繼承圖

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

圖 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 的類繼承機制,擴展的動態代理機制也有其局限,它不能支持 :

聲明為 final 的類;

聲明為 final 的函數;

構造函數均為 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

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