Androidのリモートプロシージャコール(RPC)コマンドパターン

まえがき





最近、Androidプラットフォームについて知り合いました。 特定の段階では、リモートプロシージャコール、またはより簡単にはクライアントとサーバーのやり取りで状況がどうなっているかを確認する必要がありました。



最初は、プラットフォームがEJBテクノロジーの使用を許可することが望まれていました。 インターネットでいくつか検索した後、私はそれがそれほど単純ではないと確信しました。 ほとんどのソースは、Webサービスを代替として使用することを推奨しています。 EJBはAndroidには重すぎます。 Webサービスの場合、 ksoap2-androidフレームワークが推奨されました。



ksoap2の最初の研究中に別のレーキにつまずいたので、サーバーから独自のカスタムタイプのオブジェクトを送受信する必要がある段階に達しました。 検索を使用して、 この記事をここで見つけまし 。 そこから、各カスタムオブジェクトにKvmSerializableインターフェイスを実装する必要があることを学びました。 同じことは、オブジェクトをシリアル化および逆シリアル化するためのメソッドを実装する必要があることを暗示しています。 理論上は100を超える独自のオブジェクトを使用することになっていたため、各オブジェクトに対してKvmSerializableの実装を記述するという考えは、どういうわけか私の熱意を喚起しませんでした。



何をすべきか、Androidプラットフォーム上で長年にわたってRPCを整理する便利な方法が存在しなかったことは本当にありますか? 検索は続けられました。 多くのソースがJSONの使用を推奨しています。 しかし、私もJSONのシリアル化を書きたくはありませんでした。 しかし、少し後に、 gsonライブラリーについて言及しましたが、そこはそれほど悪くないようです。



最後の希望はGWT-RPCテクノロジーでした。 GWTとAndroidは1つの企業の発案であるため、AndroidクライアントからGWT-RPCメソッドを呼び出す簡単な方法があるはずです。 残念ながら、この方法は私には見つかりませんでした。 gwt-phonegapライブラリはありますが、どういうわけかRPCに関する情報をすぐに見つけることができませんでした。



彼の検索結果にほぼ完全に失望し、彼はすでにこのビジネスを放棄したかった。 しかし、 興味深い記事がありました。 著者は、バイナリシリアル化の使用を提案しました。 Javaプラットフォームの標準であり、HTTPプロトコルとAndroidに組み込まれたApache HTTPクライアントを使用してオブジェクトを送信します。 確かに、このアプローチはすべてのオブジェクトに対して機能するとは限らないと規定されていました。 しかし、利点の中で、これにより開発時間が本当に節約されることが指摘されました。 私は著者の考えを少しテストし、多くのオブジェクトにとって、このタイプのシリアライゼーションとトランスポートが適切であると確信しました。 もちろん、多くの開発者はAndroidのバイナリシリアル化方法を悪だと言っています。 サーバーとクライアントにクラスとバージョンを保持するのは困難です。 原則として、私は大衆のために何も書くつもりはなかったので、このアプローチでは自分にとって悪いことは何も見ませんでした。 ちょっと複雑なオブジェクトごとにこのビジネスをテストする必要があることをメモしました。



トランスポートとシリアル化では、多かれ少なかれ決めたようです。 ここで、アプリケーションで使用するための便利なツールが必要でした。 それから、GWTについて、つまり対処しなければならない素晴らしいgwt-dispatchフレームワークについて、もう一度考えなければなりませんでした。 ハブラーにはすでに彼に関する良い記事がありました。 Gwt-dispatchはオープンソースプロジェクトであり、基本的にGWT RemoteServiceServlet上に構築されます。 上記の情報を分析した後、このフレームワークを通常のサーブレットのラッパーとして再作成することは可能であり、それほど難しくないように思えました。 そして、すでにAndroid側で、httpクライアントを使用して必要なメソッドを呼び出します。



私はプロジェクトのソースコードを勉強し始めました。 サーバー側を簡素化し、GWTとのすべての通信を切断する必要がありました。 すべてのActionオブジェクトは、GWT IsSerializableの代わりに通常のSerializableインターフェイスを実装する必要がありました 。 数日間の仕事の後、結果はコミュニティと共有したい結果でした。 そこで、 http-dispatchというライブラリに入れました。 そのコアは、実際にはわずかに再設計されたgwt-dispatchフレームワークです。 しかし何よりも、ライブラリはテストの準備ができており、できればAndroidプラットフォームで使用できます。 少なくともエミュレーターとタブレットでの最初のテストは成功しました。 コミュニティの助けを借りて、結果を思い起こさせることが可能になることを願っています。



序文はここで終わります。 多くの読者が実際にここに来たということです。



実用部





コマンドパターンは、クライアントが事前定義されたタイプの特定のコマンドをサーバーに送信することを意味します。 サーバーはそれを認識し、コマンドを引数として使用してそれに関連付けられたアクションを実行します。 アクションが完了すると、クライアントは特定の結果を返します。



http-dispatchフレームワークを使用して簡単なpingコマンドを作成する方法を示します。 このコマンドは、任意のオブジェクトをサーバーに送信し、同じオブジェクトを受信します。



共通のクライアントサーバー部分





まず、クライアントとサーバーの両方の操作に必要なオブジェクトについて説明します。



まず、コマンドの結果。 各結果には、 net.customware.http.dispatch.shared.Resultインターフェースを実装する必要があります。 結果はAbstractSimpleResult抽象クラスを拡張します。これは、1つのオブジェクトがサーバーから返される状況に適しています。



PingActionResult.java

import net.customware.http.dispatch.shared.AbstractSimpleResult; public class PingActionResult extends AbstractSimpleResult<Object> { private static final long serialVersionUID = 1L; public PingActionResult(Object object) { super(object); } }
      
      







次に、サーバーに送信されるコマンドを直接記述します。 後者は、前のステップで説明した結果を返します。 各チームは、汎用インターフェースnet.customware.http.dispatch.shared.Actionを実装する必要があります。 実装パラメーターは、結果のタイプを指定する必要があります。 これは、前のステップのPingActionResultになります。 私たちのチームは、サーバー上でデシリアライズされ、結果としてクライアントに送り返され、 PingActionResultにラップされたオブジェクトを含みます。 トレーニング資料でサーバーの状態のいくつかのケースを表示したいので、null結果を返し、例外をスローするオプションもチームに追加します。



PingAction.java

 public class PingAction implements Action<PingActionResult> { private static final long serialVersionUID = 1L; private Object object; //      private boolean generateException; //   null    private boolean nullResult; public PingAction(Object object) { this.object = object; } public PingAction(boolean nullResult, boolean generateException) { this.generateException = generateException; this.nullResult = nullResult; } public Object getObject() { return object; } public void setObject(Object object) { this.object = object; } public boolean isGenerateException() { return generateException; } public void setGenerateException(boolean generateException) { this.generateException = generateException; } public boolean isNullResult() { return nullResult; } public void setNullResult(boolean nullResult) { this.nullResult = nullResult; } }
      
      







この段階で、クライアントとサーバーの両方に存在する部分を特定しました。



サーバー側





まず、PingActionチームのコントロールクラスを作成します。 そのような各制御クラスは、 net.customware.http.dispatch.server.ActionHandlerインターフェースを実装する必要があります。 新しいハンドラーの作成を容易にするために、 ActionHandlerインターフェースメソッドのいくつかを既に実装している高レベルの抽象クラスSimpleActionHandlerがあります。



PingActionHandler.java

 import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import net.customware.http.dispatch.server.ExecutionContext; import net.customware.http.dispatch.server.SimpleActionHandler; import net.customware.http.dispatch.shared.ActionException; import net.customware.http.dispatch.shared.DispatchException; import net.customware.http.dispatch.test.shared.PingAction; import net.customware.http.dispatch.test.shared.PingActionResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Provider; public class PingActionHandler extends SimpleActionHandler<PingAction, PingActionResult> { protected final Logger log = LoggerFactory.getLogger(getClass()); @Override public PingActionResult execute(PingAction action, ExecutionContext context) throws DispatchException { try { //      if (action.isGenerateException()) { throw new Exception("Generated exception"); //    null  } else if (action.isNullResult()) { return null; //       } else { Object object = action.getObject(); log.debug("Received object " + object); return new PingActionResult(object); } } catch (Exception cause) { log.error("Unable to perform ping action", cause); //     ,  //       .  //    ActionException,   //   http-dispatch throw new ActionException(cause); } } // rollback         ,  //  BatchAction. ,      //   . @Override public void rollback(PingAction action, PingActionResult result, ExecutionContext context) throws DispatchException { log.debug("PingAction rollback called"); } }
      
      







次に、制御サーブレットにハンドラーを登録する必要があります。 安全でないサーブレットの例を使用して、Guiceを使用せずにこれを行う方法を初めて紹介します。



各制御サーブレットは、クラスnet.customware.http.dispatch.server.standard.AbstractStandardDispatchServletまたはAbstractSecureDispatchServletを拡張する必要があります。 標準の制御サーブレットは次のようになります



StandardDispatcherTestService.java

 import net.customware.http.dispatch.server.BatchActionHandler; import net.customware.http.dispatch.server.DefaultActionHandlerRegistry; import net.customware.http.dispatch.server.Dispatch; import net.customware.http.dispatch.server.InstanceActionHandlerRegistry; import net.customware.http.dispatch.server.SimpleDispatch; import net.customware.http.dispatch.server.standard.AbstractStandardDispatchServlet; import net.customware.http.dispatch.test.server.handler.PingActionHandler; /** */ public class StandardDispatcherTestService extends AbstractStandardDispatchServlet { private static final long serialVersionUID = 1L; private Dispatch dispatch; public StandardDispatcherTestService() { // Setup for test case InstanceActionHandlerRegistry registry = new DefaultActionHandlerRegistry(); registry.addHandler( new BatchActionHandler() ); registry.addHandler(new PingActionHandler()); dispatch = new SimpleDispatch( registry ); } @Override protected Dispatch getDispatch() { return dispatch; } }
      
      







ここでは、 PingActionHandlerに加えて、標準のBatchActionHandlerも登録します。 BatchActionコマンドパッケージを処理します



web.xmlにサーブレットの説明を追加します

web.xml

 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>DispatchServlet</servlet-name> <servlet-class>net.customware.http.dispatch.test.server.standard.StandardDispatcherTestService</servlet-class> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatchServlet</servlet-name> <url-pattern>/standard_dispatch</url-pattern> </servlet-mapping> </web-app>
      
      







それはすべてサーバー部分にあります。



お客様





クライアント側に移りましょう。 http-dispatchフレームワークがサーバーとのすべての操作が非同期であると想定することを直ちに予約します。 同期呼び出しの可能性はありますが、使用することはお勧めしません。



標準の非同期インターフェイスを使用するAndroidプラットフォーム用のシンプルなクライアントを作成します。 サーバーと対話するには、状況に応じてインターフェースnet.customware.http.dispatch.client.standard.StandardDispatchServiceAsyncまたはSecureDispatchServiceAsyncを実装する必要があります。 Androidプラットフォームでの小さな経験にもかかわらず、私はそれのためのインターフェースの簡単な実装を書く自由を取りました。 今回はnet.customware.http.dispatch.client.android.AndroidStandardDispatchServiceAsyncを使用します 。 この実装の特徴は、すべてのコマンドが個別のスレッドで実行されることです。 サーバーから結果を返すと、EDTストリームで処理が行われます。 次のようになります。



 public <R extends Result> void execute( final Action<R> action, final AsyncCallback<R> callback) { //  -"edt"    "edt"  final Handler uiThreadCallback = new Handler(); new Thread() { @Override public void run() { final Object result = getResult(action); //  і  "edt" ,   //   final Runnable runInUIThread = new Runnable() { @Override public void run() { HttpUtils.processResult(result, callback); } }; uiThreadCallback.post(runInUIThread); } }.start(); }
      
      







おそらく、このアプローチは正しくなく、私の経験が乏しいため、これを理解していません。 したがって、AndroidクライアントからRPCを呼び出すためのより適切なソリューションを示してくれた経験豊富なAndroid開発者に感謝します。



クライアントコードで、 DispatchAsyncクラスのオブジェクトを作成します。 これは次のように実行できます。



 DispatchAsync dispatch = new StandardDispatchAsync( new DefaultExceptionHandler(), new AndroidStandardDispatchServiceAsync(DISPATCH_URL_STANDARD));
      
      







サーバーにアクセスする場合、結果用にAsyncCallbackタイプのオブジェクトを作成する必要があります。



 AsyncCallback<PingRequestResult> callback = new AsyncCallback<PingRequestResult>()
      
      







メソッドを実装する



 public void onSuccess(PingRequestResult result)
      
      







そして



 public void onFailure(Throwable caught)
      
      







コマンドが正常に実行され、例外的な状況が発生すると、それに応じて呼び出されます。



次に、実行するコマンドを指定します。



 dispatcher.execute(pingRequest, callback);
      
      







RPCUtilsクラスについて説明します。RPCUtilsクラスは、サーバーに対して異なるパラメーターを使用していくつかのテスト呼び出しを行います。 DISPATCH_URLをローカルサーバーのアドレスに置き換える必要があることに注意してください。 既存のものを使用できますが、これはJBoss Openshiftプラットフォームにデプロイされたテストアプリケーションです。



RPCUtils.java

 import java.util.ArrayList; import java.util.List; import net.customware.http.dispatch.client.AsyncCallback; import net.customware.http.dispatch.client.DefaultExceptionHandler; import net.customware.http.dispatch.client.DispatchAsync; import net.customware.http.dispatch.client.android.AndroidSecureDispatchServiceAsync; import net.customware.http.dispatch.client.android.AndroidStandardDispatchServiceAsync; import net.customware.http.dispatch.client.guice.SecureDispatchModule; import net.customware.http.dispatch.client.secure.CookieSecureSessionAccessor; import net.customware.http.dispatch.client.standard.StandardDispatchAsync; import net.customware.http.dispatch.test.shared.PingRequest; import net.customware.http.dispatch.test.shared.PingRequestResult; public class RPCUtils { protected static final String DISPATCH_URL_STANDARD = "http://httpdispatch-ep.rhcloud.com/standard_dispatch"; static DispatchAsync dispatch = new StandardDispatchAsync( new DefaultExceptionHandler(), new AndroidStandardDispatchServiceAsync(DISPATCH_URL_STANDARD)); public static DispatchAsync getDispatchAsync() { return dispatch; } //     public static void runBasicStringTest(LogWrapper log) { String testObject = "Test String Object"; PingRequest pingRequest = new PingRequest(testObject); testCommon(pingRequest, log); } //      ArrayList<String> public static void runBasicListTest(LogWrapper log) { List<String> testList = new ArrayList<String>(); testList.add("one"); testList.add("two"); PingRequest pingRequest = new PingRequest(testList); testCommon(pingRequest, log); } //     null public static void runNullSubObjectTest(LogWrapper log) { PingRequest pingRequest = new PingRequest(null); testCommon(pingRequest, log); } //  null  public static void runNullObjectTest(LogWrapper log) { PingRequest pingRequest = new PingRequest(true, false); testCommon(pingRequest, log); } //       public static void runExceptionTest(LogWrapper log) { PingRequest pingRequest = new PingRequest(false, true); testCommon(pingRequest, log); } private static void testCommon(PingRequest pingRequest, final LogWrapper log) { final long start = System.currentTimeMillis(); log.log("Sending object: " + pingRequest.getObject()); DispatchAsync dispatcher = getDispatchAsync(); AsyncCallback<PingRequestResult> callback = new AsyncCallback<PingRequestResult>() { @Override public void onSuccess(PingRequestResult result) { if (result == null) { log.log("Received null at " + (System.currentTimeMillis() - start) + "ms"); } else { log.log("Received result: " + result.get() + " at " + (System.currentTimeMillis() - start) + "ms"); } } @Override public void onFailure(Throwable caught) { log.log("Received exception: " + caught.getMessage() + " at " + (System.currentTimeMillis() - start) + "ms"); } }; dispatcher.execute(pingRequest, callback); } }
      
      







LogWrapper.java

 public interface LogWrapper { void log(String text); }
      
      







アクティビティのレイアウトを作成します



activity_main.xml

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <Spinner android:id="@+id/actionTypeSP" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" /> <Button android:id="@+id/runBtn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/actionTypeSP" android:text="Run" /> <ScrollView android:id="@+id/scroller" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/runBtn" android:background="#FFFFFF" > <TextView android:id="@+id/logView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="3dp" android:scrollHorizontally="false" android:scrollbars="vertical" android:textSize="15sp" /> </ScrollView> </RelativeLayout>
      
      







さて、アクティビティ自体のコード



MainActivity.java

 import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; public class MainActivity extends Activity { enum ActionType { BASIC_STRING_OBJECT("Basic Object Send/Receive"), BASIC_ARRAYLIST_OBJECT("Basic ArrayList Send/Receive"), NULL_SUB_OBJECT("Null argument Send/Receive"), NULL_OBJECT("Null Receive"), EXCEPTION("Remote Exception") ; String description; ActionType(String description) { this.description = description; } @Override public String toString() { return description; } } Spinner actionTypeSP; TextView logView; ScrollView scroller; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } void init() { initActionList(); logView = (TextView) findViewById(R.id.logView); scroller = (ScrollView) findViewById(R.id.scroller); initRunButton(); } void initActionList() { actionTypeSP = (Spinner) findViewById(R.id.actionTypeSP); ArrayAdapter<ActionType> dataAdapter = new ArrayAdapter<ActionType>( this, android.R.layout.simple_spinner_item, ActionType.values()); dataAdapter .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); actionTypeSP.setAdapter(dataAdapter); } void initRunButton() { Button runBtn = (Button) findViewById(R.id.runBtn); final LogWrapper log = new LogWrapper() { @Override public void log(String text) { MainActivity.this.log(text); } }; runBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ActionType actionType = (ActionType) actionTypeSP .getSelectedItem(); if (actionType != null) { switch (actionType) { case BASIC_STRING_OBJECT: RPCUtils.runBasicStringTest(log); break; case BASIC_ARRAYLIST_OBJECT: RPCUtils.runBasicListTest(log); break; case EXCEPTION: RPCUtils.runExceptionTest(log); break; case NULL_OBJECT: RPCUtils.runNullObjectTest(log); break; case NULL_SUB_OBJECT: RPCUtils.runNullSubObjectTest(log); break; default: break; } } } }); } void log(String str) { String text = logView.getText().toString(); text += str + "\n"; logView.setText(text); scroller.smoothScrollTo(0, logView.getBottom()); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main, menu); return true; } }
      
      







また、アプリケーションにインターネットを使用する許可を追加することを忘れないでください



AndroidManifest.xml

 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.customware.http.dispatch.test.client.android" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" /> <uses-permission android:name="android.permission.INTERNET"/> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/title_activity_main" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
      
      







結果は次のようになります。







上のフィールドでアクションのタイプを選択してから、「実行」ボタンをクリックする必要があります。 アプリケーションはサーバーにコマンドを送信し、結果がログに表示されます。 利用可能なアクション:テスト文字列の送信、 ArrayList <String>型のオブジェクトの送信、 nullの送信、 nullの結果の取得、サーバーでの例外のスロー。



今のところすべてです。 フィードバック、コメント、訂正に感謝します。 ご清聴ありがとうございました。



参照資料





プロジェクトページ: code.google.com/p/http-dispatch

Androidテストアプリケーション: http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Android.apk

準備ができたWAR: http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Server.war

Eclipse用のAndroidアプリプロジェクト:

http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Android.zip



Gwt -dispatchフレームワーク: code.google.com/p/gwt-dispatch



PS

http-dispatchの基礎となるgwt-dispatchフレームワークは、非常に多様です。 サーバー側とクライアント側を記述する方法はいくつかあります。 次回は、Guiceと安全な制御サーブレットを使用したより興味深い例を紹介します。 さまざまな例と既製のテストアプリケーションをプロジェクトページからダウンロードできます。



All Articles