четверг, февраля 28, 2008

GWT RPC

Вплотную работаю сейчас с GWT RPC. Решил поделится приятными обнаруженными вещами пока не забыл :) , а забыть могу быстро, так как проекты с серверной частью на Java для меня скорее исключение из правил...

Для начала вкратце о том зачем оно(GWT RPC) нужно.
Все наверное слышали про AJAX (те кто не слышал могут зачитать здесь или на русском здесь). В принципе способов реализации много, но обычно(ну или часто, а не обычно - я не бог весть какой AJAX-гуру сейчас, но когда-то много чего пробовал и в итоге приходил к передаче форматированных данных, а не например html для вставки - что само по себе плохо так как стандарт на это вроде смотрит косо. Да и вообще зачем на сайте генерить то что можно переложить на клиентскую часть? Отдал данные - пусть яваскрипт работает и вставляет куда нужно что надо. При этом как правило увеличивается скорость работы, т.к. трафик получается без оверхеда - только данные без оформления гоняются.) данные передают в формате XML или JSON. Т.е. если у вас на сайте есть например объект класса BigInfo, то вы должны его преобразовать в XML или JSON и передать клиенту, а на клиенте должны соответственно обработать полученный кусок данных (часто это значит преобразовать уже в объект класса JS и сделать с ним чего-то).

Вроде всё просто но минусы тоже очевидны:
  • трудоёмкое и нудное отслеживание корректности получаемых данных(речь естественно не о простых случаях);
  • асинхронность разработки усложняет отладку и повышает вероятность ошибки(поменяв формат в серверной части и неправильно поменяв клиентскую можно получить волшебные неуловимые баги).
В принципе можно конечно написать свою волшебную библиотечку которая из класса написанного на серверном языке с некоторыми закоменченными декларациями будет генерить этот же класс но уже с методом сериализующим его например в JSON и этот же класс но уже на яваскрипте с методом десериализации из JSON. В принципе в этом есть смысл если приложение большое...

Так вот GWT RPC нужно чтобы избавить вас от всех этих проблем. Ну избавить настолько насколько это возможно. Естественно бесплатно ничего не бывает - поэтому выбора серверного языка вы лишаетесь, им автоматически становится Java. Итак вкратце как этот работает...

Для тех кто не знаком с GWT сообщу что сам GWT (безо всяких RPC) уже по сути и есть та волшебная библиотечка которая преобразует код написанный на Java в код написанный на JavaScript. Т.е. по сути нам осталось добиться того, чтобы мы могли в GWT написать серверную часть используя общие классы с клиентской частью.

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

Собственно интерфейс нашего сервиса:
public interface CustomService extends RemoteService {
public String myMethod(String s);
}
Асинхронная версия нашего сервиса, получается путём добавления к имени предыдущего интерфейса "Async", заменой возвращаемого значения на void и добавления в параметры метода аргумента типа AsyncCallback:
interface CustomServiceAsync {
public void myMethod(String s, AsyncCallback callback);
}
Ну и наконец класс который будет исполняться на серверной части, к названию основного интерфейса прибавляем "Impl" и наследуемся от RemoteServiceServlet:
public class CustomServiceImpl extends RemoteServiceServlet implements CustomService {

public String myMethod(String s) {
return s+" oO";
}
}
По сути CustomServiceImpl это сервлет умеющий делать сериализацию(т.к. наследуется от RemoteServiceServlet а не от HttpServlet).

Чтобы всё это заработало надо конечно настроить mapping для сервлета, сделать вызов с помощью асинхронного интерфейса, обработать результат в созданном callback'е. Mapping прописывается в web.xml (для Eclipse и NetBeans вроде бы по разному - так что смотрите как вам нужно)... Допустим мы замапили сервлет на url "customURL". Вызов делается примерно так:

public void testMyMethod() {
CustomServiceAsync custService = (CustomServiceAsync) GWT.create(CustomService.class);

// указываем урл по которому отзывается наш "сервлет"
ServiceDefTarget endpoint = (ServiceDefTarget) custService;
String moduleRelativeURL = GWT.getModuleBaseURL() + "customURL";
endpoint.setServiceEntryPoint(moduleRelativeURL);

// создаём callback
AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
// радуемся
}

public void onFailure(Throwable caught) {
// страдаем
}
};

// Делаем вызов нашего сервиса. Вызов будет асинхронный - программа продолжится сразу,
// а когда придёт ответ вызовется callback
custService.myMethod(someString, callback);
}
Вобщем понятно - мы получим результат в onSuccess, далее надо его преобразовать к нужному типу и обработать. Но этот пример по сути даёт только одно преимущество по сравнению со стандартным AJAX-подходом - мы может отлаживать серверную часть без отрыва от клиентской. Но тут не отображена проблема передачи не примитивов а например коллекций или объектов наших классов.

Что если мы захотим такой интерфейс, myMethod вернёт нам список строк например:
public interface CustomService extends RemoteService {
public List myMethod(String s);
}
(для тех кто не в курсе - на данный момент под GWT можно писать только в режиме совместимости с явой 1.4, поэтому List не типизирован)

Это работать не будет - потому что угадывать что за объекты лежат в List GWT не умеет, и соответственно сериализовать/десериализовать не сможет. Но для типизирования коллекций в GWT сделали специальную javadoc-аннотацию, если вы хотите получить список строк то надо переписать интерфейс так:
public interface CustomService extends RemoteService {
/**
* @gwt.typeArgs <java.lang.String>
*/
public List myMethod(String s);
}
Тут мы указываем что будет возвращён список строк а не чего-то ещё. Если надо типизировать аргумент, то пишем так:
public interface CustomService extends RemoteService {
/**
* @gwt.typeArgs listArg <java.lang.String>
* @gwt.typeArgs <java.lang.String>
*/
public List myMethod(String s,List listArg);
}
Так мы указали что listArg это список Integer, а возвращается список строк.

Итак выяснили что примитивные типы(ну и обёртки над ними) и коллекции примитивных типов сериализуемы и передавать/получать их через GWT RPC можно. Чуть сложнее дело с пользовательскими классами. Для того чтобы класс можно было сериализовать автоматически необходимо выполнение следующих условий:
  1. класс должен реализовывать интерфейс com.google.gwt.user.client.rpc.IsSerializable либо сам, либо иметь базовый класс который этот интерфейс реализовал;
  2. все поля(кроме final и transient) должны быть сериализуемы;
  3. класс должен иметь публичный конструктор по умолчанию
Если ваш класс не удовлетворяет какому-либо из этих условий, либо не устраивает стандартный сериализатор (например по производительности), то вы можете написать свой сериализатор.

Для того чтобы создать свой сериализатор для класса MyClass надо создать класс MyClass_CustomFieldSerializer (именно такое построение имени важно) такого вида:

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;

public class MyClass_CustomFieldSerializer
{
public static ServerStatusData instantiate(
SerializationStreamReader reader)
throws SerializationException
{
}

public static void serialize(
SerializationStreamWriter writer,MyClass instance)
throws SerializationException
{
}

public static void deserialize(
SerializationStreamReader reader,MyClass instance)
throws SerializationException
{
}
}
Метод instantiate() опциональный и нужен для того чтобы породить объект класса который не имеет публичного конструктора по умолчанию.

Метод serialize() позволяет записывать в поток данные которые вы хотите сериализовать. У SerializationStreamWriter(объект которого передаётся в аргументах вызова serialize()) есть методы writeBoolean, writeInt, writeObject и т.д. для этого.

Метод deserialize() соответственно позволяет считать из потока сериализованные данные и как-то привязать их к объекту. Считывать надо в том же порядке в котором вы писали в методе serialize(). У SerializationStreamReader есть методы readBoolean, readInt, readObject и т.д.

В итоге на относительно сложных проектах это всё очень удобно писать, дебагить и поддерживать.

Ссылки по теме:
GWT RPC
GWT

P.S. Правда эклипс не прижился почему-то с GWT RPC - постоянно впадал в транс при большой корректировке интерфейсов, проблемы с серверной частью у него были. Перешёл на NetBeans и пока не пожалел - костыль который я описывал здесь теперь без надобности.

8 комментариев:

Анонимный комментирует...

Дмитрий, может подскажете. Например, если есть необходимость передавать сложный класс. Я так понял, в GWT для этого надо весь класс разбить на элементарные сериализуемые типы и уже так выполнять RPC запросы персонально для каждого элемента этого класса? Или, более правильно, пересылать весь класс в виде xml объекта?

Лаврентий Палыч комментирует...

Объект сложного класса без проблем передастся автоматически если:
1. класс должен реализовывать интерфейс com.google.gwt.user.client.rpc.IsSerializable либо сам, либо иметь базовый класс который этот интерфейс реализовал;
2. все поля(кроме final и transient) должны быть сериализуемы;
3. класс должен иметь публичный конструктор по умолчанию

Соответственно да, все классы полей нестандартного типа должны быть приведены к этим условиям или для них должен быть сделан кастомный сериализатор.

Т.е. в общем случае правильно все классы полей которые передаются как части объекта сложного класса приводить к сериализуемому виду. Но на практике иногда проще написать один сериализатор для сложного класса. Т.е. тут надо смотреть уже конкретнее на задачу и на стандарты программирования которые приняты в той или иной компании.

Анонимный комментирует...

Спасибо за пост, как раз та информация, которую я искал.

webus комментирует...

Дмитрий, скажи как ты реализуешь метод сохранения объектов в БД на клиенте ? Я дак вижу это так,
public Boolean SaveObject(Object obj)
И уже на сервере определяем тип DTO объекта, и выбираем как его сохранять. Но почему то тип java.lang.Object не сериализуется через GWT RPC. Пишет ошибку.

Лаврентий Палыч комментирует...

Object не сериализуется потому что и не должен. Вот ссылка на то что сериализуется: http://code.google.com/webtoolkit/doc/1.6/DevGuideServerCommunication.html#DevGuideSerializableTypes . Там написано следуещее в том числе: "The class java.lang.Object is not serializable, therefore you cannot expect that a collection of Object types will be serialized across the wire.".

P.S. Кстати этот мой пост устарел вроде уже - например gwt.typeArgs вроде не нужны теперь... Так что лучше наверное теперь первоисточник читать, тем более там вроде всё расписали удобно.

butcherillo комментирует...

Я бы добавил, что конструктор должен быть пустым.

odis комментирует...

А есть ли возможность сосредоточить клиент и сервер в разных проектах,и что для этого нужно?

Лаврентий Палыч комментирует...

Думаю это можно сделать с помощью нескольких зависимых проектов (поиск выдал например это).

Может стоит сделать это с помощью maven. Но я давно не не использую GWT, так что не в курсе что сейчас актуально для сборки GWT проекта maven'ом - может там уже есть такое разделение прямо из коробки.