Использование удаленного вызова процедур

Все GWT приложения запускаются как код JavaScript в браузере пользователя. Тем не менее, вам достаточно часто понадобится создавать нечто большее чем просто приложение на стороне клиента. Вашему приложению понадобится связаться с сервером для отправки запросов и получения обновленных данных. Обычные web-приложения при обращении к web-серверу каждый раз загружают новую HTML страницу. С другой стороны, приложения использующие AJAX, разгружают логику пользовательского интерфейса (UI - user interface) делая асинхронные удаленные вызовы процедур, посылая и отправляее только необходимые данные. Это делает ваш пользовательский интерфейс более гибким и быстрым, уменьшая при этом требования к пропускной способности и нагрузку на сервер.

Фреймоворк GWT RPC позволяет вашему приложению легко обмениваться JAVA объектами между клиентом и сервером по HTTP. Код со стороны сервера запрошенный клиентским приложением часто называют сервисом. Имплементация сервисов GWT RPC основана на хорошо всем знакомой архитектуре Java сервлетов. На стороне клиента для вызова сервиса вы используете автоматически созданный прокси-класс. GWT получит сериализованные аргументы вызванного метода и вернет значение.

Важно отметить, что GWT RPC сервисы не то же самое что и web-сервисы основанные на SOAP или REST.

Они были разработаны как легковесный метод для передачи данных между сервером и GWT приложением на стороне клиента. Руководство разработчика содержит более подробную информацию о различных архитектурных настройках, которые вам понадобятся при создании RPC сервисов для вашего приложения.

StockPriceService (Сервис цен акциий)

Чтобы приобрести опыт работы с GWT RPC, мы добавим RPC сервис к StockWatcher. Назовем его "StockPriceService". Как вы возможно поняли по имени, этот сервис будет отправлять данные о ценах клиентскому приложению. Для простоты, мы используем уже имеющуюся логику для случайной генерации значений. Однако сейчас этот код будет запущен на сервере. Вы легко можете модфицировать сервис для работы с базой данных или какого-либо другого веб-сервиса.

Интерфейс сервиса

Первый этап в создании сервиса - интерфейс. В GWT RPC сервис определяется как интерфейс, наследуемый от интерфейса RemoteService.

В нашем интерфейс StockPriceService будет всего один метод, который будет принимать массив символов и возвращать массив объектов StockPrice. В пакете com.google.gwt.sample.stockwatcher.clientсоздаем новый файл с именем StockPriceService.java:

package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("stockPrices")
public interface StockPriceService extends RemoteService
{
  StockPrice[] getPrices(String[] symbols);
}

Единственное интересное место - аннотация @RemoteServiceRelativePath. Она ассоциирует сервис с относительным путем по умолчанию с URL модуля.

Так как StockPriceService всего лишь интерфейс, он, конечно, ничего не делает. Таким образом нам нужна имплементация сервиса.

Реализация сервиса

Реализация сервиса содержит код, который будет запущен в момент вызова сервиса. В GWT есть одно важное различие между сервисом и клиентским кодом. Так как реализация сервиса находится на сервере, она не будте транслироваться в JavaScript, как клиентский код. Сервис будет запущен как байткод Java, что означает, что сервис будет иметь полный доступ к библиотекам Java Platform и любым другим компонентам который вы захотите добавить.

Реализующий класс

Чтобы имплементировать RPC сервис, нам необходимо создать класс, имплементирующий интерфейс сервиса. Так же он должен наследоваться от RemoteServiceServlet, который содержит необходимый функционал RPC.

Давайте создадим имплементацию StockPriceService. В Eclipse, откройте диалог New Java Class (File -> New -> Class).




Условились называть имплементирующий класс так же как интерфейс, но с суффиксом Impl, таким образом мы назовем новый класс StockPriceServiceImpl. Как упоминалось раньше необходимо имплементировать интерфейс (StockPriceService) и унаследовать класс RemoteServiceServlet. Так же необходимо разместить класс в отдельном пакете (com.google.gwt.sample.stockwatcher.server). Это говорит GWT о том, что это серверный код и его не нужно транслировать в JavaScript. Как только вы нажмете Finish, новый класс будет добавлен:

import com.google.gwt.sample.stockwatcher.client.StockPrice;
import com.google.gwt.sample.stockwatcher.client.StockPriceService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class StockPriceServiceImpl extends 
RemoteServiceServlet implements
StockPriceService
{
  @Override
  public StockPrice[] getPrices(String[] symbols)
  {
    // TODO Auto-generated method stub
    return null;
  }
}


Все что осталось это реализовать в вашем единственном интерфейсе метод getPrices(String[]). Для этого мы позаимствуем код из нашего метода refreshWatchList() в StockWatcher.java.

Вот так теперь выглядит StockPriceServiceImpl.java:

package com.google.gwt.sample.stockwatcher.server;

import com.google.gwt.sample.stockwatcher.client.StockPrice;
import com.google.gwt.sample.stockwatcher.client.StockPriceService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import java.util.Random;

public class StockPriceServiceImpl extends RemoteServiceServlet implements StockPriceService
{
    private static final double MAX_PRICE = 100.0; // $100.00
    private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%

    public StockPrice[] getPrices(String[] symbols) {
        Random rnd = new Random();
        StockPrice[] prices = new StockPrice[symbols.length];
        for (int i=0; i<symbols.length; i++) {
            double price = rnd.nextDouble() * MAX_PRICE;
            double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
            prices[i] = new StockPrice(symbols[i], price, change);
        }
        return prices;
    }
}

Наша реализация сервиса выглядит привычно, но есть одно неуловимое отличие. Вы заметили import of java.util.Random? Отлично! Хотите бонус? Зачем мы сделали это изменение?

Если наш вопрос поставил вас в тупик, попробуйте всмпомнить, что этот код теперь выполняется на сервере. Это значит что мы используем "настоящий" класс java.util.Random из Java runtime library, а не его эмуляцию com.google.gwt.user.client.Random. Когда в будущем будете писать свои RPC сервисы, помните: реализация сервиса запускается как байткод Java, таким образом вы можете использовать любой Java класс или библиотеку не заботясь о том, как она будет транслирована в JavaScript.

Тестируем реализацию

Так как режим хоста дает возможность легко протестировать и сделать debug клиентской части кода. То возможно сделать тоже самое и для серверной части кода. Встроенный Tomcat сервер может хостить сервелеты содержащие реализации сервисов. Все что вам нужно сделать это добавить <servlet> в XML файл модуля указывающий на реализующий класс. Вам также нужно указать URL для реализующего класса.

URL-адрес должен быть в форме абсолютного пути к каталогу (например, /spellcheck или /common/login). Если вы указали умолчанию путь аннотацией @RemoteServiceRelativePath на интерфейс сервиса (как мы это делали с StockPriceService), вы захотите убедиться, что атрибута пути совпадает с значением (annotation value).

Довайте добавим тег <servlet> в файл StockWatcher.gwt.xml

<module>
    <!-- Inherit the core Web Toolkit stuff. -->
    <inherits name='com.google.gwt.user.User'/>
    <!-- Inherit the default GWT style sheet. You can change -->
    <!-- the theme of your GWT application by uncommenting -->
    <!-- any one of the following lines. →
    <inherits name='com.google.gwt.user.theme.standard.Standard'/>
    <!-- <inherits name="com.google.gwt.user.theme.chrome.Chrome"/> -->
    <!-- <inherits name="com.google.gwt.user.theme.dark.Dark"/> -->
    <!-- Other module inherits -->
    <!-- Specify the app entry point class. -->
    <entry-point class='com.google.gwt.sample.stockwatcher.client.StockWatcher'/>
    <servlet path="/stockPrices"
    class="com.google.gwt.sample.stockwatcher.server.StockPriceServiceImpl" />
    <!-- Specify the application specific style sheet. -->
    <stylesheet src='StockWatcher.css' />
</module>


Как только добавлен мэппинг StockPriceService на /stockPrices, полный URL будет следующим:

http://localhost:8888/com.google.gwt.sample.stockwatcher.StockWatcher/stockPrices


Хорошо, серверная часть готова, теперь поработаем над клиентским кодом. Нам нужны так называемые асинхронные вызовы.

Асинхронный вызовы

Все RPC вызовы которые вы делаете из GWT асинхронны, это значит они не заставляют вас ждать пока придет ответ. По сравнению с обычными синхронными (блокирующими) вызовами в этом есть ряд преимуществ:
  • Пользовательский интерфейс продолжает реагировать на действия пользователя. Обработчики JavaScript в браузерах как правило однопоточны, таким образом использование синхронных вызовов приведет к «зависанию» страницы до того момента, пока ответ не будет получен. Если сеть медленная или сервер не отвечает это может привести к краху фронт энда. Синхронные вызовы сервера, так же нарушают принципы AJAX (Asynchronous JavaScript And XML — Асинхронный JavaScript и XML).
  • Вы можете посылать несколько запросов на сервер в одно и то же время. Однако, браузеры как правило ограничивают число исходящих соединений до двух, ограничивая таким образом количество параллельно используемых асинхронных вызовов.
  • Вы можете совершать любые другие действия пока ждете ответа от сервера. Например, вы можете наращивать пользовательский интерфейс, походу того как получаете очередные данные. Это сокращает время проходящее между тем как пользователь запросил и увидел данные на странице.
Таким образом асинхронные вызовы очень полезны. Так как асинхронный вызов не блокирует программу, код следующий за вызовом будет выполнен немедленно. Когда вызов будет завершен, он запустит код с помощью callback-метода, который вы укажете при вызове.

Чтобы указать callback-метод, необходимо передать объект AsyncCallback проксирующему сервисному классу при вызове одного из его методов. Callback-объект должен содержать два метода: onFailure(Throwable) и onSuccess(T). Когда запрос к серверу будет завершен, один из этих методов будет вызван в зависимости от того был вызов успешным или нет.

Чтобы добавить параметр AsyncCallback ко всем сервисным методам мы создадим новый интерфейс с объявлениями методов. Эта асинхронная версия будет очень похожа на оригинальный интерфейс с небольшими отличиями:
  • Интерфейс должен называться так же как и исходный с Async на конце.
  • Он должен находиться в том же пакете что и интерфейс сервиса.
  • Каждый метод должен иметь ту же сигнатуру, но не иметь возвращаемого значения и передавать объект AsyncCallback в качестве последнего параметра.
Следуя этим правилам создадим асинхронный интерфейс для StockPriceService. Добавим новый файл с именем StockPriceServiceAsync.java в проект:

package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.AsyncCallback;
public interface StockPriceServiceAsync {
    void getPrices(String[] symbols, AsyncCallback<StockPrice[]> callback);
}


Вас может заинтересовать отсутствие возвращаемого значения. Если вы задумаетесь об этот, то поймете, что у нас просто нет выбора: так как асинхронный вызов приходит сразу же, мы просто не можем вернуть результат вызова. Итак, откуда же он придет? Ответом является параметр callback, который мы добавили только что. Если вызов будет завершен успешно возвращаемое значение будет передано в наш onSuccess(T) метод. Вы скоро сами все увидите.

Сторона клиента

Все теперь готово чтобы вызвать RPC сервис со стороны клиента. Вот что вам нужно сделать (не пытайтесь сразу набрать все, просто прочитайте сначала для ознакомления).

Создайте прокси сервисного класса

Вызовите GWT.create(Class) чтобы создать экземпляр прокси сервисного класса:
StockPriceServiceAsync stockPriceSvc = GWT.create(StockPriceService.class);

Создайте callback

Создайте экземпляр AsyncCallback.
Когда RPC вызов будет завершен, если все прошло хорошо будет вызван наш onSuccess(T)  и мы получим возвращаемое значение в аргументе result.

Если что-то пойдет не так, GWT вызовет метод onFailure(Throwable), передающий нам исключение:

AsyncCallback<StockPrice[]> callback = new AsyncCallback<StockPrice[]>() {
    public void onFailure(Throwable caught) {
        // do something with errors
    }
    public void onSuccess(StockPrice[] result) {
        // update the watch list FlexTable
    }
};

Сделайте вызов

Все что осталось, это собственно вызов:

stockPriceSvc.getPrices(symbols, callback);

Изменения в StockWatcher

Теперь когда вы знаете что делать, давайте добавим RPC-вызове в StockWatcher. Откройте файл StockWatcher.java .

Первым делом нужно добавить новые выражения import чтобы подключить необходимые классы:

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;


Далее создать поле для ссылки на прокси сервиса

private StockPriceServiceAsync stockPriceSvc;


И, наконец, заменить старый метод refreshWatchList() переделанной версией:

private void refreshWatchList() {
    // lazy initialization of service proxy
    if (stockPriceSvc == null) {
        stockPriceSvc = GWT.create(StockPriceService.class);
    }

    AsyncCallback<StockPrice[]> callback = new AsyncCallback<StockPrice[]>() {
        public void onFailure(Throwable caught) {
            // do something with errors
        }
        public void onSuccess(StockPrice[] result) {
            updateTable(result);
        }
    };

    // make the call to the stock price service
    stockPriceSvc.getPrices(stocks.toArray(new String[0]), callback);
}


Добавить новый код не так сложно. Учтите, что можно кэшировать прокси сервиса для последующих вызовов сервиса. Когда вы вызываете удаленный метод getPrices(String[]), FlexTable  содержащая ваш список будет обновлена или, в случае неудачи, ничего не произойдет (не беспокойтесь, позже мы добавим обработку ошибок).

Вы готовы увидеть GWT RPC в действии? Тогда вперед, запускайте новый StockWatcher и попытайтесь добавить новые акции в список. Так как мы поменяли XML файл модуля, вам не придется перезапускать режим хоста, достаточно просто обновить его. Приложение должно выглядеть также за исключением...


Ой.. . Этого не должно было произойти. Что же пошло не так? Глядя в лог среды разработки, мы увидим в чем проблема: наш класс StockPrice не может быть сериализован.

Сериализация

Каждый раз когда вы передаете объект по сети посредством GWT RPC, он нуждается в сериализации. Сериализация это процесс упаковки содержания объекта, так чтобы он мог быть передан из одного приложения в другое или сохранен для последующего использования. GWT RPC требует чтобы все параметры сервисных методов и возвращаемые значения были сериализуемы.

Отметим также, что в GWT сериализация и сериализация основанная на Java Serializable interface это не одно и то же.

Требования к сериализации

Что делает класс сериализуемым? Для начала, все примитивные типы (int, char, boolean, etc.) и их объекты оболочки сериализуемы по умолчанию. Массивы сериализуемых типов также сериализуемы. Классы сериализуемы если они соответствуют нескольким основным требованиям:
  1. Класс реализует IsSerializable или Serializable напрямую или наследуется от суперкласса реализующего эти интерфейсы.
  2. Он не является final или transient, объектные поля также сериализуемы и, наконец
  3. Он содержит конструктор по умолчанию (без аргументов) с любым модификатором доступа (например private Foo(){} вполне достаточно)
GWT уважает ключевое слово transient , так что значения в этих полях не сериализуемы, и не посылаются посредством RPC-вызовов.

Для любопытных, Руководство Разработчка содержит более подробную информацию о сериализации в RPC.

Исправление StockPrice

Теперь, когда мы вооруженны новыми знаниями о сериализации в GWT, давайте исправим класс StockPrice. Так как все наши поля - примитивные типы, все что нам нужно сделать это реализовать IsSerializable:

package com.google.gwt.sample.stockwatcher.client;
import com.google.gwt.user.client.rpc.IsSerializable;
public class StockPrice implements IsSerializable {

private String symbol;
private double price;
private double change;

...


Запустим StockWatcher снова. Успех! На первый взгляд ничего не изменилось, но внутри, StockWatcher получает обновленные цены со стороны сервера, от сервлета StockPriceService вместо того, чтобы генерировать их на стороне клиента. Теперь когда основа RPC работает, давайте займемся обработкой ошибок.

Обработка ошибок

В нашей обновленной реализации WatchList(), метод объекта AsyncCallback onFailure(Throwable) пока совершенно бесполезен. Он пуст, таким образом любые исключения возникшие в процессе работы RPC будут проигнорированы. Это значит, что если у пользователя отключится сетевое соединение, то цены не будут обновляться! До тех пор пока он не обратит внимание на дату последнего обновления внизу, он может и не подозревать, что видит устаревшие данные (Хм.. рынки сегодня какие-то медленные... сегодня праздник или что?). Мы не можем допустить чтобы наш пользователь торговал по неверным ценам в StockWatcher, так что давайте оповестим его если что-то пойдет не так.

Вызов GWT RPC может не сработать по одной из двух причин: по причине неожидаемых исключений или по причине проверяемых исключений.


Неожидаемые исключения

Сущестувует довольно много причин которые могут препятствовать вызову RPC. Сеть может не работать, HTTP сервер может не слушать запросы, наш DNS сервер может быть неисправен, и так далее. Когда у GWT возникает проблема с вызовом сервиса, выбрасывается InvocationException в метод onFailure(Throwable).

Другой вид неожидаемых исключений может возникать если GWT может вызвать метод, но реализация севиса выбросила необъявленное исключение. Например, может возникнуть NullPointerException. Когда возникает неожидаемое исключение в реализации сервиса, вы можете получить полный стэк в логе режима хоста:


На стороне клиента, ваш onFailure(Throwable) метод получит InvocationException с сообщением: The call failed on the server; see server log for details.

Проверяемые исключения

Если сервисный метод может выбрасывать определенный тип исключений и мы хотим иметь возможность обработать их на стороне клиента, мы будем использовать проверяемые исключения. GWT поддерживает ключевое слово throws так что вы можете добавлять его в методы интерфейса сервиса в случае необходимости. Когда проверяемые исключения возникает в RPC сервисном методе, GWT сериализует исключение и пошлет его обратно к вызвавшему на сторону клиента для обработки.


Выбрасывание исключения в StockPriceServiceImpl

Ок, давайте попробуем все это на примере. Для иллюстрации, предположим, мы хотим выбросить исключение когда пользователь пытается вернуть данные по ценам акций, которые были исключены. Первый шаг - создаем собственные класс исключения, который мы назовем DelistedException:

package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.IsSerializable;

public class DelistedException extends Exception implements IsSerializable {
  private String symbol;
  public DelistedException() {
  }
  public DelistedException(String symbol) {
    this.symbol = symbol;
  }
  public String getSymbol() {
    return this.symbol;
  }
}


Обратите внимание, что так как это исключение может быть отправлено посредством RPC мы должны отметить класс как сериализуемый с помощью IsSerializable.

Далее нам нужно добавить объявление throws в метод интерфейса сервиса в StockPriceService.java, вместе с соответствующим выраженим import:

import com.google.gwt.sample.stockwatcher.client.DelistedException;
StockPrice[] getPrices(String[] symbols) throws DelistedException;

Нам также нужно реализацию сервиса в StockPriceServiceImpl.java. Первое изменение это throws в методе getPrices(String[]). Другое изменение - это код который собственно выбрасывает DelistedException. Мы для простоты будем выбрасывать исключение каждый раз когда символ акции ERR добавлен в ползовательский список.

import com.google.gwt.sample.stockwatcher.client.DelistedException;

public StockPrice[] getPrices(String[] symbols) throws DelistedException {
    Random rnd = new Random();
    StockPrice[] prices = new StockPrice[symbols.length];
    for (int i=0; i<symbols.length; i++) {
        if (symbols[i].equals("ERR"))
            throw new DelistedException("ERR");
        double price = rnd.nextDouble() * MAX_PRICE;
        double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
        prices[i] = new StockPrice(symbols[i], price, change);
    }
    return prices;
}


Вот и все. Нет необходимости добавлять объявление throws в сервисный метод в StockPriceServiceAsync.java. Этот метод всегда возвращается мгновенно (помните, что он асинхронный), таким образом мы вместо этого получим любое выброшенное исключение когда GWT вызывает наш onFailure(Throwable).

Теперь код на стороен пользователя. Нам необходимо решить что делать когда мы ловим исключение в RPC вызове. Мы можем показать окно с собщением используя Window.alert(String), но это слишком просто. Кроме того если мы полчим много исключений пользователя может буквально закидать сообщениями об ошибках, если ошибки возникают пока он не за компьютером. Это не очень красиво. Давайте пойдем дальше и добавим новый виджет Label для вывода ошибок без лишнего беспокойства для пользователя.

Мы хотим, чтобы ошибки выделялись, так что давайте начнем с добавления нового класса в CSS в файле StockWatcher.css:

.errorMessage {
color: Red;
}


Далее, добавим виджет для вывода ошибок на экран. В StockWatcher.java добавим следующее поле:

private Label errorMsgLabel = new Label();


Инициализируем его в onModuleLoad():
...
// assemble Add Stock panel
addPanel.add(newSymbolTextBox);
addPanel.add(addButton);
addPanel.addStyleName("addPanel");
// assemble main panel
errorMsgLabel.setStyleName("errorMessage");
errorMsgLabel.setVisible(false);
mainPanel.add(errorMsgLabel);
mainPanel.add(stocksFlexTable);
mainPanel.add(addPanel);
mainPanel.add(lastUpdatedLabel);


А теперь заключительный шаг: обработка ошибок в refreshWatchList (). Добавим в onFailure (Throwable) вызова метода в следующем образом:

public void onFailure(Throwable caught) {
    // display the error message above the watch list
    String details = caught.getMessage();
    if (caught instanceof DelistedException) {
        details = "Company '" + ((DelistedException)caught).getSymbol() + "' was delisted";
    }
    errorMsgLabel.setText("Error: " + details);
    errorMsgLabel.setVisible(true);
}


Чуть не забыл: последний, последний шаг - спрятать виджет с сообщением об ошибке в конце updateTable() в случае, если ошибка была исправлена (сервер вернулся в онлайн, символ ERR был удален из таблицы и т.д.), ошбика пропадает:

private void updateTable(StockPrice[] prices) {
    for (int i=0; i<prices.length; i++) {
        updateTable(prices[i]);
    }

    // change the last update timestamp
    lastUpdatedLabel.setText("Last update : " +
    DateTimeFormat.getMediumDateTimeFormat().format(new Date()));

    // clear any errors
    errorMsgLabel.setVisible(false);
}


Настало время проверить наши изменения. Запустите StockWatcher (изменения встепят в силу если мы обновим окно браузера в режиме хоста, после того как модифицировали сервис) и добавим символ ERR в список просмотра:


Если вы переключитесь на окно оболочки разработчика (Development Shell window), вы не увидите сообщений об исключениях в логе, так как исключения были проверены (и, соответственно, ожидаемы).

Развертывание сервисов на рабочем сервере

В процессе тестирования, вы можете пользоваться встроенным Tomcat сервером в режиме хоста (hosted mode) для тестирования кода на стороне сервера. Когда вы будуте деплоить ваше GWT приложение, вы можете использовать любой контейнер сервлетов для ваших сервисов. Просто убедитесь что ваш клиентский код вызывает сервис используя URL на который прицеплен сервлет в web.xml конфигурациооном файле. За более подробной информацией о деплое RPC сервлетов смотрите эту статью.

Дополнительная информация
Чтобы узнать больше читайте секцию Remote Procedure Calls в руководстве разработчика.

Комментариев нет:

Отправить комментарий