📜 ⬆️ ⬇️

Command Pattern Remote Procedure Call (RPC) in Android

Foreword



Recently, I began my acquaintance with the Android platform. At a certain stage, it was necessary to check how things are going with the remote procedure call, or, more simply, client-server interaction.

At first, it was hoped that the platform would allow the use of EJB technology. After some searching on the Internet, I made sure that it is not so easy. Most sources recommend using web services as an alternative, since EJB is too heavy for Android. For web services, the ksoap2-android framework was recommended.
')
Having run into various rakes during the initial study of ksoap2, I reached the stage when it was necessary to send and receive an object of my custom type from the server. Using the search found this article . From there, I learned that every custom object must implement the KvmSerializable interface. This implied that we must implement methods for serializing and deserializing an object. Since the theory was supposed to use more than a hundred of its own objects, the idea of ​​writing the KvmSerializable implementation for each of them somehow did not arouse my enthusiasm.

What to do, really for so many years on the Android platform there is no more convenient way to organize RPC? The search continued. Many sources recommended using JSON. But I also didn’t really want to write serialization for JSON. A little later, however, I stumbled upon the mention of the gson library, and it seems there is not so bad there.

The last hope was for the GWT-RPC technology. It was assumed that since GWT and Android are the brainchild of a single corporation, there probably should be an easy way to call GWT-RPC methods from the Android client. Unfortunately, this method was not found by me. Although there is a library gwt-phonegap, but somehow I just could not find information on RPC.

Almost completely disappointed in the results of his searches, I wanted to abandon the case. But then there was an interesting article . The author suggested using binary serialization, i.e. standard for the Java platform, and send objects using the HTTP protocol and the Apache HTTP Client built into Android. It is true there stipulated that the approach may not work for all objects. But among the advantages indicated that it really saves development time. I tested the author's idea a little bit and made sure that this kind of serialization and transport is more than suitable for many objects. Of course, many developers describe the binary serialization method for Android as evil, because it's hard to keep up with the same version classes on the server and client. In principle, I was not going to write anything for the masses, so I didn’t see anything wrong with this approach. Only made a note that it is necessary to test this case for each new less complex object.

It seems that I have more or less decided on transport and serialization. Now I wanted to have some convenient tool for use in the application. Then I had to remember about GWT again, namely about the wonderful gwt-dispatch framework with which I already had to deal. On Habré was already a good article about him. Gwt-dispatch is an open source project and, in fact, it builds on the GWT RemoteServiceServlet. After analyzing the above information, it seemed to me that it would be possible and not very difficult to remake this framework as a wrapper over a regular servlet. And on the Android side, call the necessary methods using the http client.

I began to study the source code of the project. It was necessary to simplify the server part, and break any connection with the GWT. Now all Action objects had to implement the usual Serializable interface instead of the GWT IsSerializable . After several days of work on the output, I got the result that I wanted to share with the community. Therefore, I designed it into a library called http-dispatch . At its core, it is actually a slightly reworked gwt-dispatch framework. But best of all, the library is ready for testing and, I hope, for use on the Android platform. At least the first tests in the emulator and on my tablet were successful. I hope that with the help of the community, the result can be brought to mind.

At this preface ends. I mean that many readers still came here for the practical part.

Practical part



The command pattern implies that the client sends a certain predefined type command to the server. The server recognizes it and performs the action associated with it using the command as an argument. After completing the action, a certain result is returned to the client.

I'll show you how to write a simple ping command using the http-dispatch framework. The command will send an arbitrary object to the server and receive the same object back.

Common client-server part



First of all, we will describe the objects necessary for the work of both the client and the server.

First, the result of the command. Each result must implement the net.customware.http.dispatch.shared.Result interface. Our result will extend the abstract class AbstractSimpleResult , which is suitable for situations when a single object is returned from the server.

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); } } 


Now we will write directly the command that will be sent to the server. The latter, in turn, will return the result described in the previous step. Each team must implement the generic net.customware.http.dispatch.shared.Action interface. The implementation parameter must specify the type of result. This will be the PingActionResult from the previous step. Our team will contain an object that is deserialized on the server and sent back to the client already as a result wrapped in a PingActionResult . Since in the training material you want to show several cases of state on the server, we will add to our team also the options for returning null results and throwing an exception.

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; } } 


At this stage, we have dealt with the part that must be present both on the client and on the server.

Server part



First of all, let's write a management class for our PingAction team. Each such control class must implement the net.customware.http.dispatch.server.ActionHandler interface. There is a higher-level abstract class, SimpleActionHandler , which already implements some methods of the ActionHandler interface to make it easier to write new handlers.

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"); } } 


Now we need to register the handler in the managing servlet. For the first time, I'll show you how to do this without using Guice with the example of an insecure servlet.

Each control servlet must extend the net.customware.http.dispatch.server.standard.AbstractStandardDispatchServlet or AbstractSecureDispatchServlet class. Our standard managing servlet will look like this.

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; } } 


Here, in addition to our PingActionHandler, we also register the standard BatchActionHandler . It serves to process the BatchAction command set .

Now we add the description of our servlet to 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> 


On this, perhaps, with the server part all.

Customer



Let's go to the client side. At once I will make a reservation that the http-dispatch framework assumes that all operations with the server are asynchronous. Although there is a possibility of a synchronous call, but I would not recommend using it.

We write a simple client for the Android platform, which will use the standard asynchronous interface. To interact with the server, you must implement the interface net.customware.http.dispatch.client.standard.StandardDispatchServiceAsync or SecureDispatchServiceAsync , depending on the situation. Despite my limited experience with the Android platform, I took the liberty to write a simple interface implementation for it. This time we will use net.customware.http.dispatch.client.android.AndroidStandardDispatchServiceAsync . A feature of this implementation is that all commands are executed in a separate thread. Upon returning the same result from the server, processing occurs in the EDT stream. It looks like this:

 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(); } 


Perhaps this approach is not correct and I do not understand it in view of my scant experience. Therefore, I would be very grateful to more experienced Android developers, if they show a better solution for calling RPC from an Android client.

In the client code, we create an object of the class DispatchAsync . You can do this as follows:

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


When accessing the server, you need to create an object of type AsyncCallback for our result.

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


and implement methods

 public void onSuccess(PingRequestResult result) 


and

 public void onFailure(Throwable caught) 


which will be called accordingly when the command is successful and when an exception occurs.

Then we give the command to execute:

 dispatcher.execute(pingRequest, callback); 


We describe the class RPCUtils , which will make several test calls with different parameters on our server. Remember, you will need to send a DISPATCH_URL to your local server address. Although you can use the existing one - it is a deployed test application on the JBoss Openshift platform.

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); } 


Now create layout for our Activity.

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> 


Well, the code of the Activity itself

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; } } 


Also, do not forget to add permission to use the Internet to our application.

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> 


The result will look like this:



You need to select the type of action in the upper field and then click the Run button. The application will send a command to the server and the result will be displayed in the log. The available actions are: send a test string, send an object of type ArrayList <String> , send null , get a null result, and generate an exception on the server.

That's all for now. I would be grateful for any feedback, comments and amendments. Thank you all for your attention.

Links



Project Page: code.google.com/p/http-dispatch
Test application for Android: http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Android.apk
Ready WAR: http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Server.war
Android project for Eclipse:
http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Android.zip

Gwt -dispatch framework: code.google.com/p/gwt-dispatch

PS
The gwt-dispatch framework used in http-dispatch is quite diverse. There are several ways to write the server and client side. Next time, I'll show you a more interesting example using Guice and a secure managing servlet. Various examples and ready-made test applications can be downloaded from the project page.

Source: https://habr.com/ru/post/148436/


All Articles