GWT →  Использование шаблона Command для организации RPC-вызовов в GWT

В своем прошлогоднем выступлении в рамках Google I/O Ray Rayan поведал аудитории о том, как правильно стоить архитектуру более-менее крупных GWT-проектов. Одна из его рекомендаций — использование шаблона (паттерна) Command для оргиназации RPC-сервисов. В данной заметке я постараюсь вкратце осветить данный подход на примере простейшего GWT-приложения. Для диспетчеризации RPC-вызовов будет использована библиотека gwt-dispatch GWT-Dispatch. Сразу хочу предупредить, что эта статья является симбиозом, осмыслением и компиляцией нескольких источников (GWT-Dispatch Getting Started, GWT MVP Example). Рассматривайте ее как руководство к быстрому старту на пути правильного построения GWT-приложений. Весь материал разработан с учетом того, что серверная реализация RPC-сервисов также выполняется на языке Java.

Не секрет, что при разработке более менее крупных приложений нам на помощь спешат шаблоны (паттерны) проектирования. Паттерны являются своего рода рецептами решения конкретных типовых случаев. Начать старт в изучении и применении паттернов проектирования можно с Patterns on Wiki и дальше углубляться уже в соответствующие книги, статьи, труды и т. д.
Если быть кратким, то шаблон Command (Команда) позволяет выполнять конкретные реализации интерфейса Command (Action etc.) через унифицированный интерфейс.
Command UML Diagram
Касательно GWT RPC применение этого подхода позволит иметь один интерфейс вызовов RPC-сервисов (диспетчер) и в него передавать объект соответствующего действия (команды).

Подключение необходимых библиотек

Итак, для реализации RPC-взаимодействия в проекте с помощью команд нам понадобиться подключить к проекту дополнительные библиотеки:
  • GWT-Dispatch - GWT-реализация диспетчера вызовов. Также эта библиотека предоставляет интерфейсы Action и Result для организации своих команд и их результатов выполнения.
  • Google Guice - Dependency Injection-фреймворк от Google. Позволяет организовать управление зависимостями в серверном коде с помощью Dependency Injection-подхода. Он намного проще всем известного Spring Framework, и, соответственно, работает быстрее. При реализации демо-проекта Guice сослужил также службу, как диспетчер сервлетов и инициализирующее звено всего сервер-сайда. Но об этом немного позже.
  • Google GIN — реализация Guice для GWT. Позволяет применять DI-подход в клиентском (читай, GWT) коде. Его явно мы использовать не будем, он требуется как зависимость.
Для подключения этих библиотек к проекту достаточно положить файлы gin-1.0.jar, guice-2.0.jar, guice-servlet-2.0.jar и gwt-dispatch-1.0.0.jar в WEB-INF/lib и добавить их в Build Path проекта. Конфигурация GWT-модуля с подключенными модулями у меня выглядит так:
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='rpc_command'>
	<inherits name='com.google.gwt.user.User' />
	<inherits name="com.google.gwt.inject.Inject" />
	<inherits name="net.customware.gwt.dispatch.Dispatch" />

	<entry-point class='net.pimgwt.client.RpcCommandEntryPoint' />

	<source path='client' />
</module>

Организация конкретной команды на GWT стороне

Создание команды я проиллюстрирую на примере создания одного RPC-вызова. Его суть будет проста как дверь: отправить серверному методу считанный из поля ввода параметр и получить ответ. Все. Ах да, отобразить полученный ответ на UI. В демо-проекте содержится еще вызов, который от сервера получает массив фейковых DTO-объектов. Его код не будет рассмотрен в этой заметке. Если интересно, я его могу предоставить дополнительно в комментариях.
Для создания RPC-команды нужно создать класс, который реализует интерфейс Action:
package net.pimgwt.client.rpc;

import net.customware.gwt.dispatch.shared.Action;

@SuppressWarnings("serial")
public class SingleRequestAction implements Action<SingleRequestResult> {
	private String param;

	public SingleRequestAction() {
	}

	public SingleRequestAction(String param) {
		this.param = param;
	}

	public String getParam() {
		return this.param;
	}
}
Как видим, ничего сложного. Эта команда инкапсулирует в себе параметр, который будет передан на сервер. Единственный здесь интересный момент — это указание того, что результатом выполнения будет объект класса SingleRequestResult:
package net.pimgwt.client.rpc;

import net.customware.gwt.dispatch.shared.Result;

@SuppressWarnings("serial")
public class SingleRequestResult implements Result {
	private String resultMessage;

	public SingleRequestResult() { }

	public SingleRequestResult(String resultMessage) {
		this.resultMessage = resultMessage;
	}

	public String getResultMessage() {
		return this.resultMessage;
	}
}
который также инкапсулирует в себе данные, которые «приедут» клиентскому коду.
На данный момент приготовления на клиентской стороне закончены. Самое время взяться за поджаривание кофейных зерен, которые будут работать на сервере. Кстати, сервер у нас будет работать на Google App Engine.

Серверная реализация RPC-сервисов

Библиотека GWT-Dispatch предоставляет инструментарий для организации диспетчеров как для клиентской, так и для серверной частей.
Начнем конфигурирование сервер-сайда с диспетчера сервлетов, которые будут заниматься обработкой RPC-вызовов:
package net.pimgwt.server;

import net.customware.gwt.dispatch.server.service.DispatchServiceServlet;

import com.google.inject.servlet.ServletModule;

public class DispatcherServletModule extends ServletModule {
	@Override
	protected void configureServlets() {
		serve("/rpc_command/dispatch").with(DispatchServiceServlet.class);
	}
}
Класс DispatcherServletModule наследник ServletModule. В нем переписан родительский метод configureServlets(), который устанавливает соответствие RPC-URL реализации диспетчера сервлетов, который предоставляется GWT-Dispatch. По-умолчанию, URL, который будет прослушиваться диспетчером строится по схеме имя_приложения/dispatch.
Реализуем теперь обработчик (handler), который будет привязан к команде, объявленной на клиентской стороне (команда SingleRequestAction):
package net.pimgwt.server;

import net.customware.gwt.dispatch.server.ActionHandler;
import net.customware.gwt.dispatch.server.ExecutionContext;
import net.customware.gwt.dispatch.shared.ActionException;
import net.pimgwt.client.rpc.SingleRequestAction;
import net.pimgwt.client.rpc.SingleRequestResult;

public class SingleRequestHandler implements ActionHandler<SingleRequestAction, SingleRequestResult> {
	@Override
	public SingleRequestResult execute(SingleRequestAction action, ExecutionContext
			context) throws ActionException {
		return new SingleRequestResult("You are entered: " + action.getParam());
	}

	@Override
	public Class<SingleRequestAction> getActionType() {
		return SingleRequestAction.class;
	}

	@Override
	public void rollback(SingleRequestAction action, SingleRequestResult result,
		ExecutionContext context) throws ActionException {	}
}
Обработчик команды реализует generic-интерфейс, при параметризации которого указываются команда и ее результат. В данном случае это SingleRequestAction и SingleRequestResult соответственно. Интерфейс ActionHandler также обязует класс-реализацию предоставить методы execute(), getActionType() и rollback(), названия которых говорят сами за себя. В приведенном коде для такой простой команды, как SingleRequestAction действие отката в случае неудачи просто оставлено пустым. Нечего откатывать.
Результатом выполнения метода execute() является объект SingleRequestResult, в который мы просто записываем текст ответа, который будет передан вызывающей (клиентской) стороне.
Ну и метод getActionType() должен вернуть ссылку на класс команды, к которой привязан обработчик. Это нужно для того, чтобы диспетчер смог корректно вызвать нужный обработчик, а не какой-то другой.
Помимо непосредственно диспетчеризации и предоставления интерфейсов Action и Result библиотека GWT-Dispatch также предоставляет интеграцию с Google Guice. Эта интеграция позволяет зарегистрировать обработчики команд в Guice-контексте:
package net.pimgwt.server;

import net.customware.gwt.dispatch.server.guice.ActionHandlerModule;

public class RpcCommandHandlerModule extends ActionHandlerModule {
	@Override
	protected void configureHandlers() {
		bindHandler(SingleRequestHandler.class);
		// . . .
	}
}
Свяжем все воедино с помощью класса GuiceServletContextListener, который будет «слушать» происходящее извне и реагировать в том случае, когда от клиента будет происходить запрос /rpc_command/dispatch и запускать обработчик соответствующей команды:
package net.pimgwt.server;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;

public class RpcCommandGuiceConfig extends GuiceServletContextListener {
	@Override
	protected Injector getInjector() {
		return Guice.createInjector(new RpcCommandHandlerModule(), new DispatcherServletModule());
	}
}
Класс GuiceServletContextListener предоставляется фреймворком Guice как средство его интеграции с Java Servlets. Приведенный код выполнит все необходимые инъекции (injects) в нужные места. Таким образом у нас цепочка интеграции GWT-Dispatch и Guice и с Servlets будет замкнута.
Последний шаг, который нужен для того, чтобы все это заиграло как единый ансамбль – указание в web.xml файле нужного слушателя и соответствующий фильтр запросов:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
	<filter>
		<filter-name>guiceFilter</filter-name>
		<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>guiceFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<listener>
		<listener-class>net.pimgwt.server.RpcCommandGuiceConfig</listener-class>
	</listener>

	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
</web-app>
GuiceFilter настроен на фильтрацию всех запросов, попадающих на серверную сторону от клиента. Есстественно, в url-param-инструкции можно указать свой шаблон URL для прослушивания. Как это делать не скажу, это очевидные вещи и они не имеют отношения к рассматриваемому вопросу.
Серверная часть готова. Осталось теперь связать RPC-вызовы с клиентского кода с диспетчером.

Диспетчеризация команд в GWT-коде

За вызовы RPC-команд в GWT-коде отвечает интерфейс DispatchAsync. Вы можете выполнить реализацию сего интерфейса как пожелаете, например, как диспетчер, который умеет кешировать полученные ранее результаты. Для демо-проекта я выбрал «коробочную» реализацию DefaultDispatchAsync опять же из поставки GWT-Dispatch.
Ниже я приведу только обработчик нажатия на кнопке, который инициирует RPC-вызов через указанный интерфейс и отображает полученный от серверной стороны результат:
// . . .
private DispatchAsync rpcDispatcher = new DefaultDispatchAsync();
// . . .
@UiField Button singleValueTestButton;
// . . .

@UiHandler("singleValueTestButton")
public void singleValueButtonClicked(ClickEvent event) {
	responseLable.setText("");

	rpcDispatcher.execute(new SingleRequestAction(paramTextbox.getText()), new
			AsyncCallback<SingleRequestResult>() {
		@Override
		public void onFailure(Throwable caught) {
			responseLable.setText("Error occured: " +
				caught.getMessage());
		}

		@Override
		public void onSuccess(SingleRequestResult result) {
			responseLable.setText(result.getResultMessage());
		}
	});
}
Основной момент здесь в том, что мы передаем диспетчеру инициализированную команду для отправки ее на сервер. В коллбеке из полученного отвера SingleRequestResponse просто извлекается результат: responseLable.setText(result.getResultMessage());

Все написано, реализовано, настроено и даже работает!

Демо-проект

Ниже на скриншоте показана структура демо-проекта в панеле Project Packages
Project Packages
Если присмотреться к нему, то можно увидеть, что в проекте реализована еще одна RPC-команда, MultiRequestAction. Резальтатом ее выполнения является MultiRequestResult, который в свою очередь содержит List<DummyDTO>, который наполняется в цикле в сервеном обработчике этой команды.
Проект для live-просмотра доступен RPC Command Demo Project

Вместо заключения

Описанный подход RPC-взаимодействия не умаляет роли простых RPC-вызовов, которые немного были рассмотрены в статье Авторизация через службу User Service в GWT приложениях. В некоторых случаях, когда у вас в проекте один, максимум два обращения к серверной стороне то особого смысла городить огород из Action-ов, Result-ов, каких-то Guice и иже с ними не имеет смысла, потому что только усложняет код. С другой стороны, применения "правильных" практик построения ООП-кода повышает его структурируемость, читаемость и _добавьте свой бенефит_.
Более того, мне известны несколько проектов на GWT, которые вообще на серверной стороне не содержат Java. Значит при такой серверной реализации есстественно применять какой-то общий формат обмена сообщениями, например, JSON или XML. Но это уже другая история.

Жду конструктивной критики, пожеланий и, конечно же, вопросов!
Спасибо.

добавить комментарий: