Командный паттерн вызова удаленных процедур (RPC) в Android

в 5:42, , рубрики: android, Dispatch, guice, java, RPC, Разработка под android, метки: , , , ,

Предисловие

Недавно я начал свое знакомство с платформой Android. На определенном этапе необходимо было проверить, как тут обстоят дела с удаленным вызовом процедур или, проще говоря, клиент-серверным взаимодействием.

Поначалу была надежда, что платформа позволит использовать технологию EJB. После некоторых поисков в Интернете, я убедился, что это не так просто. Большинство источников рекомендовало использовать вебсервисы как альтернативу, т.к. EJB слишком тяжеловесна для Android. Для вебсервисов же рекомендовался фреймворк ksoap2-android.

Понатыкавшись на различные грабли при первоначальном изучении ksoap2, я дошел до этапа, когда необходимо было послать и получить с сервера объект своего кастомного типа. Воспользовавшись поиском нашел вот эту статью. Оттуда почерпнул, что каждый кастомный объект должен реализовать интерфейс KvmSerializable. Это же подразумевало, что мы должны реализовать методы для сериализации и десериализации объекта. Поскольку в теории предполагалось использовать больше сотни собственных объектов, идея писать реализацию KvmSerializable для каждого из них как-то не вызывала у меня энтузиазма.

Что же делать, неужели за столько лет на платформе Android нет более удобного способа для организации RPC? Поиски продолжались. Многие источники рекомендовали использовать JSON. Но писать сериализацию для JSON мне тоже не очень то и хотелось. Чуть позже, правда, я наткнулся на упоминание о библиотеке gson, и, кажется, там с этим делом не так плохо.

Последняя надежда была на технологию GWT-RPC. Предполагалось, что раз GWT и Android детища одной корпорации, то, вероятно, должен существовать простой способ вызывать методы GWT-RPC из Android клиента. К сожалению, такой способ мною найден не был. Хотя и существует библиотека gwt-phonegap, но как-то сходу мне не удалось найти информации касательно RPC.

Практически полностью разочаровавшись в результатах своих поисков, хотел было уже забросить это дело. Но тут нашлась интересная статья. Автор предлагал использовать бинарную сериализацию, т.е. стандартную для платформы Java, и посылать объекты используя HTTP протокол и Apache HTTP Client встроенный в Android. Правда там оговаривалось, что подход может работать не для всех объектов. Но среди преимуществ указывалось, что это реально экономит время разработки. Немного потестировал идею автора и убедился, что для многих объектов такой вид сериализации и транспорта более чем подходит. Конечно, многие разработчики описывают метод бинарной сериализации для Android как зло, т.к. сложно уследить, чтобы на сервере и клиенте были классы одинаковой версии. В принципе, я не собирался писать ничего для широких масс, поэтому для себя не узрел ничего плохого в таком подходе. Только сделал пометку, что надо тестировать это дело для каждого нового более менее сложного объекта.

Похоже с транспортом и сериализацией я более-менее определился. Теперь же хотелось иметь какой-нибудь удобный инструмент для использования в приложении. Тут пришлось опять вспомнить про GWT, а именно про замечательный фреймворк gwt-dispatch, с которым мне уже приходилось иметь дело. На хабре уже была хорошая статья по поводу него. Gwt-dispatch — это проект с открытым кодом и он, по сути, надстраивается над GWT RemoteServiceServlet. Проанализировав вышеизложенную информацию, мне показалось, что будет возможно и не очень сложно переделать этот фреймворк как обертку над обычным сервлетом. А уже на стороне Android вызывать необходимые методы используя http клиент.

Я принялся изучать исходники проекта. Необходимо было упростить серверную часть, и разорвать всякую связь с GWT. Теперь все объекты Action должны были реализовать обычный интерфейс Serializable вместо GWT IsSerializable. После нескольких дней работы на выходе получился результат, которым мне захотелось поделиться с сообществом. Поэтому я оформил его в библиотеку под названием http-dispatch. По своей сути она фактически является немного переделанным фреймворком gwt-dispatch. Но что самое приятное, библиотека готова к тестированию и, надеюсь, использованию на Android платформе. По крайней мере первые тесты в эмуляторе и на моей таблетке прошли успешно. Надеюсь что при помощи сообщества результат удастся довести до ума.

На этом предисловие заканчивается. Подразумеваю, что многие читатели все же зашли сюда за практической частью.

Практическая часть

Командный паттерн подразумевает, что клиент отправляет на сервер некую команду предопределенного типа. Сервер распознает ее и выполняет сопоставленное с ней действие используя команду в качестве аргумента. После выполнения действия клиенту возвращается некий результат.

Я покажу вам как написать простенькую ping команду используя фреймворк http-dispatch. Команда будет посылать произвольный объект на сервер и получать такой же объект обратно.

Общая клиент-серверная часть

В первую очередь опишем объекты необходимые для работы как клиента так и сервера.

Сначала результат выполнения команды. Каждый результат должен реализовать интерфейс net.customware.http.dispatch.shared.Result. Наш же результат будет расширять абстрактный класс AbstractSimpleResult, который подходит для ситуаций, когда с сервера возвращается один объект.

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

Теперь напишем непосредственно команду, которая будет отправляться на сервер. Последний же, в свою очередь, будет возвращать результат описанный в предыдущем шаге. Каждая команда должна реализовать generic интерфейс 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. Существует более высокоуровневый абстрактный класс SimpleActionHandler, который уже реализует некоторые методы интерфейса ActionHandler для облегчения написания новых обработчиков

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, который будет делать несколько тестовых вызовов с разными параметрами на наш сервер. Помните, вам будет необходимо заменть 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);
}

Теперь создадим layout для нашего 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>

Ну и код самого Activity

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>

Выглядеть результат будет следующим образом:

Командный паттерн вызова удаленных процедур (RPC) в Android

Неоходимо выбрать тип действия в верхнем поле и затем нажать кнопку Run. Приложение отправит команду на сервер и в логе выведется полученный результат. Доступные действия: отправить тестовую строку, отправить объект типа 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
Проект Android приложения для Eclipse:
http-dispatch.googlecode.com/files/HTTP_Dispatch_Test_Android.zip

Фреймворк gwt-dispatch: code.google.com/p/gwt-dispatch/

P.S.
Фреймворк gwt-dispatch, взятый за основу в http-dispatch, довольно многообразен. Существует несколько способов написания сервеной и клиентской части. В следующий раз я покажу вам более интересный пример с использованием Guice и безопасного управляющего сервлета. Различные примеры и готовые тестовые приложения можно скачать на странице проекта.

Автор: Evgenij_Popovich

* - обязательные к заполнению поля