JCA 1.5: 消息輸入流

火星人 @ 2014-03-10 , reply:0


EJB 2.0 消息驅動 bean

  在使用 J2EE 1.3 和 EJB 2.0 時,消息驅動 bean(MDB)只能起到相當有限的作用.它們讓應用程序非同步地接收傳遞 JMS 目標的消息.而 MDB 需要實現 javax.jms.MessageListener,如清單 1 所示:

  清單 1. javax.jms.MessageListener 介面

1 public interface MessageListener {
2 void onMessage(Message message);
3 }

  MDB 還需要實現 MessageDrivenBean 介面.結果,典型的類看起來可能類似清單 2 中的 ExampleMdb :

  清單 2. 示例 EJB 2.0 MDB

1 public class ExampleMdb implements MessageDrivenBean, MessageListener {
2 public void ejbCreate() throws CreateException { ... }
3 public void ejbRemove() { ... }

4 public void setMessageDrivenContext(MessageDrivenContext ctx) { ... }
5 public void onMessage(Message message) {
6 if (msg instanceof TextMessage) {
7 String text = ((TextMessage) message).getText();
8 InitialContext context = new InitialContext();
9 ExampleHome home = (ExampleHome)context.lookup("java:comp/env/ejb/Example");
10 Example bean = home.create();
11 bean.operation(text);
12 }
13 }

14 }

  如果像清單 2 那樣遵照 EJB 2.0 規範的建議,那麼 onMessage 方法負責對消息的內容進行解包,然後調用其他 EJB 執行實際的業務邏輯.

  但是別誤會 —— EJB 2.0 MDB 當然有它們的好處.對於初學者來說,因為 MDB 不是由客戶機調用的,不需要編寫 home、remote 或 local 介面. —— 最主要的一個好處就是,多個實例可以并行操作.在沒有 MDB 時,應用程序可能已經確定了一個非同步接收消息的目標,但是沒有將工作複製到另外一個線程中的能力,應用程序須先處理第一個消息才能處理下一個消息.

  就像其他 EJB 那樣,MDB 能夠使用 bean 託管事務或者容器託管事務.後者對 MDB 有一些扭曲.,只允許使用兩個事務屬性:Required 和 NotSupported.這是完全合理的.因為沒有客戶機調用 MDB,沒有需要繼承的事務,要做的就是能夠指出 bean 是否應當在事務中運行.第二,也是更重要的,如果指定了事務屬性 Required,那麼傳遞到 MDB 的消息就作為消息的一部分接收.如果有什麼事情出錯,事務將回滾,那麼消息將被重新放在目標上,再次進行處理.

  正如在清單 3 中可以看到的,MDB 只用於 JMS 的這個事實也反映在部署描述符中:

  清單 3. EJB 2.0 部署描述符

1 <ejb-jar>
2 <enterprise-beans>
3 <message-driven>
4 <ejb-name>Example MDB</ejb-name>

5 <ejb-class>example.ExampleMDB</ejb-class>
6 <transaction-type>Bean</transaction-type>
7 <acknowledge-mode>Auto-acknowledge</acknowledge-mode>
8 <message-driven-destination>
9 <destination-type>javax.jms.Topic</destination-type>
10 <subscription-durability>Durable</subscription-durability>

11 </message-driven-destination>
12 <message-selector>
13 JMSType = 'car' AND color = 'red'
14 </message-selector>
15 </message-driven>
16 </enterprise-beans>
17 </ejb-jar>
18

  在清單 3 中可以看到,部署描述符中的一些元素對應著 JMS 概念中的應答模式、訂閱、持久性和消息選擇器.另外一個元素給出了消息目標的類型,它是 javax.jms.Queue 或者是 javax.jms.Topic.

EJB 2.1 消息驅動 bean

  那麼在 J2EE 1.4 的 EJB 2.1 中發生了什麼變化呢?有一個重大的區別:MDB 現在可以實現任何介面.沒錯 —— 任何 介面.例如,JCA 公共客戶端介面(CCI)定義了清單 4 所示的介面,希望由 CCI 連接器非同步驅動的 MDB 應當實現這些介面:

  清單 4. javax.resource.cci.MessageListener 介面

1 public interface MessageListener {
2 Record onMessage(Record inputData)
3 throws ResourceException;
4 }

  CCI 介面展示了新的靈活性的一些好處.,不需要把 MDB 傳遞給 JMS 消息.在清單 4 的介面中,傳入一個 Record 對象.JMS 消息到達目標時也無需觸發方法調用.對於 MDB 會在什麼情況下驅動,JCA 規範沒有做任何限制.它可以是資源適配器接收到後端系統的一些外部刺激之後的結果,也可以僅僅是一些內部事件,甚至可能是由計時器驅動的事件.第二,如清單 4 所示,調用的方法也可以擁有返回參數.在這種情況下,就可以傳遞迴第二個 Record 對象,正如清單 5 的 ExampleListener 介面中的第一個方法所示,類型無需相同:

  清單 5. listener 介面示例

1 public interface ExampleListener {
2 ExampleOutput process(ExampleInput input);
3 String getData();
4 void request(int id, ExampleRequest request);

5 }

  通常應當用方法的返回參數向造成方法調用事件的發起者提供響應,不論它是資源適配器還是其他什麼系統.但是,就像 ExampleListener 介面的第二個方法所表現的那樣,實際上可以不用任何參數調用 MDB,只用它檢索一些值.第三個方法表明:方法可以使用的參數個數沒有限制.這意味著資源適配器可以把初始事件解析為將傳遞給 MDB 的多個值.可以看到,方法的參數可以是原生類型或是對象.,即使 ExampleListener 介面有多個方法,使用它也是正確的.這樣就可以讓資源適配器根據事件判斷要調用哪個方法.

  既然 MDB 介面中消除了與 JMS 有關的限制,那麼將部署描述符放在哪呢?清單 6 顯示了一個 EJB 2.1 部署描述符示例,針對的是實現 ExampleListener 介面的 MDB:

  清單 6. EJB 2.1 部署描述符

1 <ejb-jar>
2 <enterprise-beans>
3 <message-driven>
4 <ejb-name>Example MDB</ejb-name>

5 <ejb-class>example.ExampleMdb</ejb-class>
6 <messaging-type>example.ExampleListener</messaging-type>
7 <transaction-type>Bean</transaction-type>
8 <activation-config>
9 <activation-config-property>
10 <activation-config-property-name>
11 HostName

12 </activation-config-property-name>
13 <activation-config-property-value>
14 example.com
15 </activation-config-property-value>
16 </activation-config-property>
17 <activation-config-property>
18 <activation-config-property-name>
19 DefaultId
20 </activation-config-property-name>
21 <activation-config-property-value>

22 2001
23 </activation-config-property-value>
24 </activation-config-property>
25 </activation-config>
26 </message-driven>
27 </enterprise-beans>
28 </ejb-jar>
29

  在清單 6 中可以看到,messaging-type 元素包含 MDB 實現的那些介面的類名.如果遺漏了這個元素,那麼可以採用 javax.jms.MessageListener 的舊值.特定於 JMS 的元素已經被一組通用的名稱-值對替代,這個名稱-值對叫作 激活配置屬性(activation-configuration properties).在清單 6 中可以看到,為 HostName 和 DefaultId 屬性分別指定了值 example.com 和 2001.

  可以編寫實現任意介面的 MDB,這聽起來太棒了,感覺不像是真的——可是它確實是真的.接下來的事可能就不太讓人驚訝了:需要找到一個準備調用這個介面的資源適配器.資源適配器用它的部署描述符中的 messagelistener-type 元素表示它準備支持的介面(可能不止一個介面),如清單 7 所示:

  清單 7. 輸入資源適配器的部署描述符

1 <connector>
2 <display-name>Example Inbound Resource Adapter</display-name>
3 <vendor-name>Example COM</vendor-name>
4 <eis-type>Example EIS</eis-type>
5 <resourceadapter-version>1.0</resourceadapter-version>
6 <resourceadapter>

7 <resourceadapter-class>
8 example.ExampleRaImpl
9 </resourceadapter-class>
10 <inbound-resourceadapter>
11 <messageadapter>
12 <messagelistener>
13 <messagelistener-type>example.ExampleListener</messagelistener-type>
14 <activationspec>
15 <activationspec-class>example.ExampleActivationSpecImpl</activationspec-class>

16 <required-config-property>
17 <config-property-name>HostName</config-property-name>
18 </required-config-property>
19 <required-config-property>
20 <config-property-name>PortNumber</config-property-name>
21 </required-config-property>
22 </activationspec>

23 </messagelistener>
24 </messageadapter>
25 </inbound-resourceadapter>
26 </resourceadapter>
27 </connector>
28

  詳細看看這個示例部署描述符是值得的.它從一些傳統的東西開始:顯示名稱、供應商名稱、資源適配器連接的企業信息系統(EIS)的名稱,以及適配器的版本.這些內容後面跟著一個 resourceadapter 元素,它的第一個子元素包含 ResourceAdapter 介面的適配器實現的名稱.後面跟著的是 inbound-resourceadapter 元素,它可以包含多個 messageadapter 實例.每個 messageadapter 實例都與一個 messagelistener-type 相關聯.通過擁有多個 messageadapter 元素,可以編寫一個輸入資源適配器,支持多個介面.但是,要保證每個 messageadapter 指定一個不同的介面.

  每個 messageadapter 還包含實現 ActivationSpec 介面的類的名稱.在下一節中,我將介紹如何用這個類包含每個部署的 MDB 的配置信息.

部署 MDB

  正如在前面一節看到的,資源適配器支持的每個 MDB 介面都給出實現 ActivationSpec 介面的類的名稱,如清單 8 所示:

  清單 8. javax.resource.spi.endpoint.ActivationSpec 介面

1 public interface ActivationSpec extends ResourceAdapterAssociation {

2 void validate() throws InvalidPropertyException;
3 }

  這個實現類遵循 JavaBean 標準,可以通過適當指定的 getter 和 setter 方法來定義大量屬性.在將 MDB 部署到應用伺服器 style="COLOR: #000000" href="http://server.it168.com/" target=_blank>伺服器中時,將使用這個類,如下所示:

  應用伺服器通過查看 MDB 部署描述符中 messaging-type 元素的內容,判斷 MDB 實現的介面.

  然後應用伺服器找到支持這個介面的已經部署的資源適配器,選擇一個想在上面部署 MDB 的適配器.

  應用伺服器創建 ActivationSpec 類的實例,這個類對應於選中的資源適配器和介面,然後使用內省(introspection)功能確定類的屬性以及默認值.

  用 ResourceAdapter 的父類上同名配置的屬性覆蓋默認值.

  可以選擇覆蓋這些屬性值中的任何值.

  然後這些屬性被 MDB 部署描述符中的 activation-config 屬性的值再次覆蓋.

  應用伺服器檢測要求在資源適配器部署描述中指定的所有 bean 屬性(在這個示例中,是 HostName 和 PortNumber)都有提供的值.

  可以看到,部署的 MDB 最終使用的屬性可以來自許多地方,重要的是記住它們的先後順序.特別是要注意 MDB 的部署描述符不需要包含資源適配器要求的所有屬性.

  一旦所有屬性設置就緒,應用伺服器就可以選擇調用 ActivationSpec 上的 validate 方法,或者把這個方法的調用推遲到應用程序啟動時.資源適配器可能用這個方法檢查算術屬性是否在可以接受的範圍之內,或者確保表示枚舉值的字元串有效.資源適配器還應當檢查的屬性集是否一致;例如,可能要根據其他屬性的值來判定某個屬性的值是否受限或是必需的.InvalidPropertyException 包含一項或多項檢測失敗時需要拋出的一組無效屬性.

  成功的驗證並不意味著部署的 MDB 肯定能成功啟動.資源適配器只能執行靜態檢測,也就是說,執行那些不需要連接到後台系統的檢測.只有當應用程序啟動時,資源適配器才能驗證類似於主機名稱和埠號這樣的屬性是否正確.

  MDB 生命周期管理

  一旦成功地把包含 MDB 的應用程序部署到應用伺服器中,下一步就是啟動應用程序.應用伺服器用清單 9 中的 ResourceAdapter 介面中的兩個方法,向資源適配器通知關於某個具體 MDB 生命周期的事件(在規範中稱之為 端點):

  清單 9. ResourceAdapter 介面上的端點生命周期方法

1 public interface ResourceAdapter {
2 void endpointActivation(MessageEndpointFactory endpointFactory,
3 ActivationSpec spec) throws ResourceException;
4 void endpointDeactivation(MessageEndpointFactory endpointFactory,
5 ActivationSpec spec);
6 ...
7 }

  在啟動應用程序時,要為每個部署的 MDB 調用一次 endpointActivation 方法.第一個參數(我將在下一節詳細介紹)是一個創建端點實例的工廠類.方法的第二個參數是在前一節中配置的 ActivationSpec.如果以前沒有做過,那麼現在可以這樣做,此刻,資源適配器通常使用來自 ActivationSpec 的信息與後端系統建立某種形式的遠程連接.如果在做這項工作的過程中,資源適配器判定配置的信息不正確,那麼它將拋出 NotSupportedException.

  注意,資源適配器不應當阻塞在這個方法中.一旦它確定配置是正確的,則應當立即返回.如果需要進行進一步處理(例如為新的事件定期申請連接),那麼資源適配器應當使用 WorkManager 介面,把工作安排到另外一個線程上.

  在包含 MB 的應用程序被停止時,或者處在應用伺服器的正常停機期間,會調用對應的 endpointDeactivation 方法.傳遞給這個方法的參數就是在 endpointActivation 上傳遞的同一個對象.實際上,因為 JCA 規範強制要求,對於每個端點激活都要創建 MessageEndpointFactory 的一個新實例,在不活動(on deactivation)期間傳遞的對象可以用作在兩者之間進行相互關聯的鍵.例如,在激活時創建的資源可能放在一個切斷與 MessageEndpointFactory 的聯繫的映射中,然後在伺服器不活動的時候,再檢索出這些資源,對它們進行清理.

  調用 MDB

  現在所有工作都已經就緒,到了資源適配器實際調用 MDB 的時候了.在使用 J2EE 1.3 時,這一步可能將使用一個相當麻煩的應用伺服器工具,該工具是 JMS 規範的一個組成部分.交互很複雜,在某些地方,交互的指定也很糟,從而造成不同的廠商用不同的方式對需求進行解釋.幸運的是,JCA 規範不僅讓 MDB 能夠實現任何介面,還極大地簡化了這一過程中的事物.

  清單 10 顯示了傳遞給 endpointActivation 方法的對象實現的 MessageEndpointFactory:

  清單 10. javax.resource.spi.endpoint.MessageEndpointFactory 介面

1 public interface MessageEndpointFactory {
2 boolean isDeliveryTransacted(Method method) throws NoSuchMethodException
3 MessageEndpoint createEndpoint(XAResource xaResource) throws UnavailableException;
4 }

  isDeliveryTransacted 方法讓資源適配器判斷關聯的 MDB 是否運行在事務中調用指定方法.如果 MDB 正在使用容器託管的事務,要處理的方法具有 Required 事務屬性,那麼 isDeliveryTransacted 會返回 true .如果事務屬性採用 NotSupported 允許的其他值,或者 MDB 正在使用 bean 託管的事務,則返回 false.

  我將在下一節中介紹事務.此刻,假設 isDeliveryTransacted 返回了 false (或者假設資源適配器不支持事務).這意味著在調用 createEndpoint 方法時,可以把 null 作為 XAResource 的參數傳入.這個方法返回一個實現了 MessageEndpoint 介面的對象,如清單 11 所示:

  清單 11. javax.resource.spi.endpoint.MessageEndpoint 介面

1 public interface MessageEndpoint {
2 void release();
3 void beforeDelivery(Method method)
4 throws NoSuchMethodException, ResourceException
5 void afterDelivery() throws ResourceException;
6 }

  返回的對象還實現了 MDB 在它的部署描述符中聲明的介面.在最簡單的情況下,資源適配器就可以把 MessageEndpoint 的類型轉換成必要的介面,然後調用預期方法.

  createEndpoint 返回的對象顯然不是應用程序開發人員實現的類的實例,因為這個類沒有實現 MessageEndpoint 介面.相反,它是應用伺服器創建的一個代理.應用伺服器可能使用了 Java 1.4 引入的動態代理支持,即時創建對象,它也可能在部署應用程序時就創建了必要的類.容器使用代理來包裝實際的端點,以便提供事務和安全性這樣的服務.這裡比較一下代理和對無狀態 bean 本地介面的引用.在使用會話 bean 的情況下,只能調用在本地介面中聲明的方法.類似地,對於代理,也只能調用在 MDB 的部署描述符中聲明的介面上的方法.

  資源適配器可以選擇保持在一個端點上,以便在後續調用中使用它.或者,資源適配器在完成任務並為下一調用創建另一個端點時,也可以調用 release.通常,應用伺服器保持一個端點池,第二個選項或許是為了在多個線程之間提供更好的重用而提供的.

  我說過這代表的是簡單情況.JCA 規範把這稱作 Option A 消息傳遞.作為何時需要可供替代的 Option B 消息傳遞的一個例子,可以將資源適配器想像為代表應用程序傳輸序列化的 Java 對象的消息傳遞系統的一部分.希望調用的 MDB 方法採用反序列化之後的對象作為參數.糟糕的是,與序列化對象對應的類是 J2EE 程序程序的一部分,因此該類位於應用程序的類路徑中,而不是位於資源適配器的類路徑中.Option B 用 beforeDelivery 方法和 afterDelivery 方法圓滿地解決了這個問題.調用 beforeDelivery 會提前執行一些容器操作(這些操作正常情況下可能是在調用代理和調用實際端點方法之間發生的).這些操作包括把應用程序的類載入器和線程關聯起來.調用 beforeDelivery 之後,資源適配器就可以用線程適配器將對象反序列化,並把對象傳遞給 MDB 介面上要求的方法.

  容器知道自己已經執行了一些在這個端點代理上要求的操作,會在調用實際的 MDB 方法時跳過這些方法.但是,被調用的方法和 beforeDelivery 上傳遞的 Method 對象匹配,否則就會拋出 RuntimeException.在調用該方法之後,資源適配器調用 afterDelivery 來執行方法調用之後應當發生的對應的容器操作.例如,這樣就允許適配器將方法返回的對象序列化,不過仍然是通過使用應用程序的類載入器來實現的.

  圖 1 顯示了兩個消息傳遞選項的並列式比較.

  圖 1. 消息傳遞選項

  

  對於 Option A,可以看到前調用邏輯和后調用邏輯是作為一個方法調用的部分執行的,而對於 Option B,它們被分開,分別由 beforeDelivery 和 afterDelivery 驅動.

  MDB 和事務

  如果資源適配器支持 XA (全局) 事務, isDeliveryTransacted 表明 MDB 期望容器啟動一個事務,那麼資源適配器就應當在調用 createEndpoint 時傳入一個 XAResource 介面的實現.XA 事務是個複雜的主題,與 JCA 無關的問題超出本文的範圍.對於更多細節,請參考 Java 事務 API 規範 .

  對於如何進行處理,資源適配器有兩個選項.它可以選擇把要執行的事務性工作直接與它的 XAResource 對象關聯起來.資源適配器就可以使用 Option A 消息傳遞.當調用代理時,容器會調用 start,列舉出事務中的 XAResource.這個時候,資源適配器可以把指定的 Xid 與要執行的工作關聯起來.代理繼續調用端點實例.在端點方法返回之後,容器就完成了事務.當 XAResource 接收到對 prepare 的調用時,它應當保證自己能夠在返回 XA_OK 之前成功完成要求的工作.,XAResource 接收 commit 或 rollback,這時它應當提交或取消已經執行的工作.

  或者,資源適配器也可以使用 Option B 消息傳遞,在這種情況下,事務是作為 beforeDelivery 的一部分啟動的.這樣就允許資源適配器在調用 MDB 的方法之前從 XAResource 檢索 Xid.例如,如果在自己能夠構建傳遞給方法的參數之前,資源需要把 Xid 傳遞給後端系統,那麼這可能是必需的.可以想像,在這種情況下,事務是在調用 afterDelivery 的時候完成的.

  如果應用伺服器在調用 prepare 和 commit、rollback 之間失敗,那麼在伺服器重啟時需要進行事務恢復.為了執行恢復,應用程序伺服器需要為每個資源管理器獲取一個 XAResource.應用伺服器實現這一目標的方式是:在調用 prepare 之前,把與端點關聯的 ActivationSpec 寫入永久性存儲 style="COLOR: #000000" href="http://storage.it168.com/" target=_blank>存儲器.在恢復的時候,伺服器讀出表示每個資源適配器的 ActivationSpec 對象列表,這些適配器對應著它認為沒有完成的事務.這個對象數組將傳遞給 ResourceAdapter 介面上的 getXAResources 方法,如清單 12 所示:

  清單 12. ResourceAdapter 介面上的事務恢復方法

1 public interface ResourceAdapter {
2 XAResource[] getXAResources(ActivationSpec[] specs)
3 throws ResourceException;
4 ...
5 }

  應用伺服器調用每個 XAResource 對象上的 recover 方法來確定資源管理器準備好的事務.然後根據情況為每個事務調用 commit 或 rollback.

  在前文中,介紹了 ExecutionContext 如何與 WorkManager 結合,把事務導入應用伺服器.如果用導入的事務調用具有事務屬性 Required 的端點方法,那麼這個方法將繼承該事務.在這種情況下,任何 XAResource passed on createEndpoint 上傳遞的方法都不會列在該事務中.

  託管對象

  在一節中,我將介紹的一個主題是:可以平等地用於輸入和輸出 資源適配器,雖然這一主題在 JCA 規範的消息輸入流一章中層介紹過.規範的 1.5 版本的目標之一就是允許 JMS 提供者作為 JCA 資源適配器實現.如果了解 JMS,那麼就會注意到它包含兩類可以管理的對象:連接工廠和目標.通過託管連接工廠和它們在資源適配器的部署描述符中定義的屬性,JCA 提供了一種機制,使管理員可以定義連接工廠,以便部署的程序能夠通過 Java 名稱與目錄服務介面(JNDI)查找它們.但是 JMS 目標不是工廠,而是擁有不同屬性(描述它們表示的隊列或主題)的對象.如果這樣,那麼怎麼才能創建 JMS 目標呢?

  JCA 1.5 規範定義了託管對象 來彌補這個空白.資源適配器的部署描述符可以包含一個或多個 adminobject 元素,如清單 13 所示:

  清單 13. 顯示託管對象的部署描述符代碼段

1 <adminobject>
2 <adminobject-interface>example.ExampleAdminObject</adminobject-interface>
3 <adminobject-class>example.ExampleAdminObjectImpl</adminobject-class>
4 <config-property>

5 <config-property-name>Color</config-property-name>
6 <config-property-type>java.lang.String</config-property-type>
7 <config-property-value>Red</config-property-value>
8 </config-property>
9 <config-property>
10 <config-property-name>Depth</config-property-name>

11 <config-property-type>java.lang.Integer</config-property-type>
12 </config-property>
13 </adminobject>
14

  在這種情況下,應用程序期望從 JNDI (通過資源環境引用)查找實現了 example.ExampleAdminObject 介面的對象.部署描述符提供了能提供這個對象實現的 JavaBean 類的名稱.它還提供了許多命名屬性,每個屬性都有一個類型和一個可選的默認值,這種方式非常類似於託管連接工廠.當管理員希望定義一個對象時,應用程序伺服器的工具應當允許管理員選擇資源適配器和託管對象介面,然後提示管理員輸入每個屬性的值.應用伺服器隨後將包含這個信息的引用綁定到 JNDI.當應用程序執行查找時,就創建一個託管對象的實例,在把對象返回應用程序之前設置好屬性.

  現在應當能夠看出 JCA 資源適配器如何通過使用託管對象來提供 JMS 目標對象了.但是請不要忘記,您可以對任何想讓管理員能夠配置的 JavaBean(不僅僅是連接工廠)使用託管對象,這些 JavaBean 可以通過 JNDI 訪問.

  結束語

  在本文中,我解釋了消息驅動 bean 在 J2EE 1.3 和 J2EE 1.4 之間的變化,特別是目前如何實現任何介面.您已經看到如何用 JCA 消息輸入流合約來配置資源適配器,以便用它來檢索 MDB 實例以及調用 MDB 上的方法.我還詳細介紹了調用 bean 方法的選項,以及調用事務性方法的意義.,我介紹了託管對象如何使用資源適配器定義將要配置的 JavaBean,並使其能夠通過 JNDI 訪問.

  這一文章系列介紹了 JCA 1.5 的一些新特性,從優化和生命周期管理開始,到工作管理和事務,以消息輸入流合約告終.像過去一樣,規範提供了信息的確定來源,肯定會在未來的版本中繼續發展.我希望您覺得這個系列有用,不管您是打算升級現有的資源適配器,還是打算從頭編寫一個新的資源適配器,或者僅僅把資源適配器用作應用程序的一部分.




[火星人 via ] JCA 1.5: 消息輸入流已經有109次圍觀

http://www.coctec.com/docs/java/show-post-61950.html