В своем прошлогоднем выступлении в рамках 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.) через унифицированный интерфейс.



Касательно 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-модуля с подключенными модулями у меня выглядит так:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <module rename-to='rpc_command'>
  3.   <inherits name='com.google.gwt.user.User' />
  4.   <inherits name="com.google.gwt.inject.Inject" />
  5.   <inherits name="net.customware.gwt.dispatch.Dispatch" />
  6.  
  7.   <entry-point class='net.pimgwt.client.RpcCommandEntryPoint' />
  8.  
  9.   <source path='client' />
  10. </module>
* This source code was highlighted with Source Code Highlighter.


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


Создание команды я проиллюстрирую на примере создания одного RPC-вызова. Его суть будет про��та как дверь: отправить серверному методу считанный из поля ввода параметр и получить ответ. Все. Ах да, отобразить полученный ответ на UI. В демо-проекте содержится еще вызов, который от сервера получает массив фейковых DTO-объектов. Его код не будет рассмотрен в этой заметке. Если интересно, я его могу предоставить дополнительно в комментариях.
Для создания RPC-команды нужно создать класс, который реализует интерфейс Action:
  1. package net.pimgwt.client.rpc;
  2.  
  3. import net.customware.gwt.dispatch.shared.Action;
  4.  
  5. @SuppressWarnings("serial")
  6. public class SingleRequestAction implements Action<SingleRequestResult> {
  7.   private String param;
  8.  
  9.   public SingleRequestAction() {
  10.   }
  11.  
  12.   public SingleRequestAction(String param) {
  13.     this.param = param;
  14.   }
  15.  
  16.   public String getParam() {
  17.     return this.param;
  18.   }
  19. }
* This source code was highlighted with Source Code Highlighter.

Как видим, ничего сложного. Эта команда инкапсулирует в себе параметр, который будет передан на сервер. Единственный здесь интересный момент — это указание того, что результатом выполнения будет объект класса SingleRequestResult:
  1. package net.pimgwt.client.rpc;
  2.  
  3. import net.customware.gwt.dispatch.shared.Result;
  4.  
  5. @SuppressWarnings("serial")
  6. public class SingleRequestResult implements Result {
  7.   private String resultMessage;
  8.  
  9.   public SingleRequestResult() { }
  10.  
  11.   public SingleRequestResult(String resultMessage) {
  12.     this.resultMessage = resultMessage;
  13.   }
  14.  
  15.   public String getResultMessage() {
  16.     return this.resultMessage;
  17.   }
  18. }
* This source code was highlighted with Source Code Highlighter.

который также инкапсулирует в себе данные, которые «приедут» клиентскому коду.
На данный момент приготовления на клиентской стороне закончены. Самое время взяться за поджаривание кофейных зерен, которые будут работать на сервере. Кстати, сервер у нас будет работать на Google App Engine.

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


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

  1. package net.pimgwt.server;
  2.  
  3. import net.customware.gwt.dispatch.server.service.DispatchServiceServlet;
  4.  
  5. import com.google.inject.servlet.ServletModule;
  6.  
  7. public class DispatcherServletModule extends ServletModule {
  8.   @Override
  9.   protected void configureServlets() {
  10.     serve("/rpc_command/dispatch").with(DispatchServiceServlet.class);
  11.   }
  12. }
* This source code was highlighted with Source Code Highlighter.

Класс DispatcherServletModule наследник ServletModule. В нем переписан родительский метод configureServlets(), который устанавливает соответствие RPC-URL реализации диспетчера сервлетов, который предоставляется GWT-Dispatch. По-умолчанию URL, который будет прослушиваться диспетчером строится по схеме имя_приложения/dispatch.
Реализуем теперь обработчик (handler), который будет привязан к команде, объявленной на клиентской стороне (команда SingleRequestAction):
  1. package net.pimgwt.server;
  2.  
  3. import net.customware.gwt.dispatch.server.ActionHandler;
  4. import net.customware.gwt.dispatch.server.ExecutionContext;
  5. import net.customware.gwt.dispatch.shared.ActionException;
  6. import net.pimgwt.client.rpc.SingleRequestAction;
  7. import net.pimgwt.client.rpc.SingleRequestResult;
  8.  
  9. public class SingleRequestHandler implements ActionHandler<SingleRequestAction, SingleRequestResult> {
  10.   @Override
  11.   public SingleRequestResult execute(SingleRequestAction action, ExecutionContext
  12.       context) throws ActionException {
  13.     return new SingleRequestResult("You are entered: " + action.getParam());
  14.   }
  15.  
  16.   @Override
  17.   public Class<SingleRequestAction> getActionType() {
  18.     return SingleRequestAction.class;
  19.   }
  20.  
  21.   @Override
  22.   public void rollback(SingleRequestAction action, SingleRequestResult result,
  23.     ExecutionContext context) throws ActionException {  }
  24. }
* This source code was highlighted with Source Code Highlighter.

Обработчик команды реализует generic-интерфейс, при параметризации которого указываются команда и ее результат. В данном случае это SingleRequestAction и SingleRequestResult соответственно. Интерфейс ActionHandler также обязует класс-реализацию предоставить методы execute(), getActionType() и rollback(), названия которых говорят сами за себя. В приведенном коде для такой простой команды, как SingleRequestAction действие отката в случае неудачи просто оставлено пустым. Нечего откатывать.
Результатом выполнения метода execute() является объект SingleRequestResult, в который мы просто записываем текст ответа, который будет передан вызывающей (клиентской) стороне.
Ну и метод getActionType() должен вернуть ссылку на класс команды, к которой привязан обработчик. Это нужно для того, чтобы диспетчер смог корректно вызвать нужный обработчик, а не какой-то другой.
Помимо непосредственно диспетчеризации и предоставления интерфейсов Action и Result библиотека GWT-Dispatch также предоставляет интеграцию с Google Guice. Эта интеграция позволяет зарегистрировать обработчики команд в Guice-контексте:
  1. package net.pimgwt.server;
  2.  
  3. import net.customware.gwt.dispatch.server.guice.ActionHandlerModule;
  4.  
  5. public class RpcCommandHandlerModule extends ActionHandlerModule {
  6.   @Override
  7.   protected void configureHandlers() {
  8.     bindHandler(SingleRequestHandler.class);
  9.     // . . .
  10.   }
  11. }
* This source code was highlighted with Source Code Highlighter.

Свяжем все воедино с помощью класса GuiceServletContextListener, который будет «слушать» происходящее извне и реагировать в том случае, когда от клиента будет происходить запрос /rpc_command/dispatch и запускать обработчик соответствующей команды:
  1. package net.pimgwt.server;
  2.  
  3. import com.google.inject.Guice;
  4. import com.google.inject.Injector;
  5. import com.google.inject.servlet.GuiceServletContextListener;
  6.  
  7. public class RpcCommandGuiceConfig extends GuiceServletContextListener {
  8.   @Override
  9.   protected Injector getInjector() {
  10.     return Guice.createInjector(new RpcCommandHandlerModule(), new DispatcherServletModule());
  11.   }
  12. }
* This source code was highlighted with Source Code Highlighter.

Класс GuiceServletContextListener предоставляется фреймворком Guice как средство его интеграции с Java Servlets. Приведенный код выполнит все необходимые инъекции (injects) в нужные места. Таким образом у нас цепочка интеграции GWT-Dispatch и Guice и с Servlets будет замкнута.
Последний шаг, который нужен для того, чтобы все это заиграло как единый ансамбль – указание в web.xml файле нужного слушателя и соответствующий фильтр запросов:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE web-app
  3.   PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  4.   "http://java.sun.com/dtd/web-app_2_3.dtd">
  5.  
  6. <web-app>
  7.   <filter>
  8.     <filter-name>guiceFilter</filter-name>
  9.     <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  10.   </filter>
  11.   <filter-mapping>
  12.     <filter-name>guiceFilter</filter-name>
  13.     <url-pattern>/*</url-pattern>
  14.   </filter-mapping>
  15.  
  16.   <listener>
  17.     <listener-class>net.pimgwt.server.RpcCommandGuiceConfig</listener-class>
  18.   </listener>
  19.  
  20.   <welcome-file-list>
  21.     <welcome-file>index.html</welcome-file>
  22.   </welcome-file-list>
  23. </web-app>
* This source code was highlighted with Source Code Highlighter.
GuiceFilter настроен на фильтрацию всех запросов, попадающих на серверную сторону от клиента. Есстественно, в url-param-инструкции можно указать свой шаблон URL для прослушивания. Как это делать не скажу, это очевидные вещи и они не имеют отношения к рассматриваемому вопросу.
Серверная часть готова. Осталось теперь связать RPC-вызовы с клиентского кода с диспетчером.

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


За вызовы RPC-команд в GWT-коде отвечает интерфейс DispatchAsync. Вы можете выполнить реализацию сего интерфейса как пожелаете, например, как диспетчер, который умеет кешировать полученные ранее результаты. Для демо-проекта я выбрал «коробочную» реализацию DefaultDispatchAsync опять же из поставки GWT-Dispatch.
Ниже я приведу только обработчик нажатия на кнопке, который инициирует RPC-вызов через указанный интерфейс и отображает полученный от серверной стороны результат:
  1. // . . .
  2. private DispatchAsync rpcDispatcher = new DefaultDispatchAsync();
  3. // . . .
  4. @UiField Button singleValueTestButton;
  5. // . . .
  6.  
  7. @UiHandler("singleValueTestButton")
  8. public void singleValueButtonClicked(ClickEvent event) {
  9.   responseLable.setText("");
  10.  
  11.   rpcDispatcher.execute(new SingleRequestAction(paramTextbox.getText()), new
  12.       AsyncCallback<SingleRequestResult>() {
  13.     @Override
  14.     public void onFailure(Throwable caught) {
  15.       responseLable.setText("Error occured: " +
  16.         caught.getMessage());
  17.     }
  18.  
  19.     @Override
  20.     public void onSuccess(SingleRequestResult result) {
  21.       responseLable.setText(result.getResultMessage());
  22.     }
  23.   });
  24. }
* This source code was highlighted with Source Code Highlighter.

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

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

Демо-проект


Ниже на скриншоте показана структура демо-проекта в панели Project Packages

хостинг картинок

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

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


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

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