Pull to refresh

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

Reading time 10 min
Views 7.4K
В своем прошлогоднем выступлении в рамках 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. Но это уже другая история…

Жду конструктивной критики, пожеланий и, конечно же, вопросов!
Спасибо.
Tags:
Hubs:
+6
Comments 6
Comments Comments 6

Articles