08 maja 2007

GWT RPC raz jeszcze, RequestBuilder oraz Timer

Na razie idzie niezwykle gładko, zdecydowanie za dobrze. Tak kończyłem ostatni wpis o GWT i muszę przyznać, że z każdym dniem, kiedy zasiadam do GWT coraz bardziej się upewniam, że jest to właściwa technologia kliencka do dalszej ewaluacji. Chciałbym, aby z każdym szkieletem programistycznym (*) było tak przyjemnie.

(*) Nie przekonuje mnie użycie tłumaczenia szkielet programistyczny jako odpowiednik angielskiego framework. Z chęcią zmieniłbym je na coś bardziej dźwięcznego. Może pora na nowe słówko?! Tyle ich powstaje, a my ciągle szukamy tłumaczeń.

Sukcesem dnia dzisiejszego w kategorii GWT jest stworzenie namiastki aplikacji pod szumnie brzmiącym tytułem Kółko i krzyżyk. Mimo, że zgodnie z wypowiedzią Reinier Zwitserloot na forum GWT - How do I start ?:

run a continuous connection**: You can't create a 'live' connection between webapp and client. Let's say you want to write 2 player pacman in GWT, with the two players connected through your server: Can't really be done; every time a player hits a key, you have to submit a complete HTTP request, with at least 200 bytes worth of headers and the like in there.

sądziłem, że jednak się da i...się nie udało. To znaczy, udało się jedynie zasymulować grę z pewnymi ograniczeniami, które powodują, że po 3 dniach walki stwierdzam, że Reinier miał w 100% rację ;-)

Mimo nieodpowiedniego przykładu do zebrania doświadczeń w GWT poznałem wiele ciekawostek związanych z tym środowiskiem. Możliwość skorzystania z Apache Maven 2 do zarządzania tworzeniem aplikacji dodatkowo uprościło przedsięwzięcie i pozwoliło mi na skupienie się na detalach GWT, a nie wszystkiego wokół, poza GWT (jak to zazwyczaj bywa).

Zanim rozpiszę się na dobre wspomnę, że archiwum forum GWT jest nieocenionym źródłem informacji. W zasadzie są tam wszystkie pytania z odpowiedziami, które samemu zadawałem sobie próbując nagiąć się do myślenia w tonie GWT. Technologia jest na tyle młoda, jednakże wystarczająco stabilna do tworzenia części klienckiej aplikacji, że śmiało można powiedzieć, że niewiele jest dostępnych aplikacji, które możnaby potraktować jako wzorcowe. Pozostaje, więc skorzystanie z archiwum forum i przeszukiwanie Internetu (prawdopodobnie również z Google).

Dla przypomnienia, co wciąż jednak umykało mojej uwadze, jest fakt, że GWT to tworzenie aplikacji myśląc o niej w kanonach JFC/Swing, gdzie (zdalne) usługi wykonywane są na serwerze i traktowane jako źródło danych (i nic poza tym), a część kliencka będzie wykonywana w przeglądarce jako JavaScript. Ponownie, wspominany już, Reinier Zwitserloot trafił w dziesiątkę ze swoim opisem części serwerowej w przewrotnie nazwanym wątku JRuby on Grails?:

GWT turns your server into a dumb terminal for data.

, co odzwierciedla problemy, których doświadczałem próbując współdzielić dane między klientami. Początkowo wykorzystałem do tego servlet i dane umieszczane w zasięgu application. Przez długi czas nie mogłem się z tym pogodzić, że wymieniając dane między klientami korzystam z ServletContext. Zazwyczaj przechowuję w nim dane konfiguracyjne bądź statyczne dane, a dane dynamiczne umieszczam w zasięgu sesji (session scope) bądź najczęściej (do czego przekonało mnie JSF) zlecenia (request scope). Okazało się, że to, co należało obsłużyć za pomocą "czystego" GWT RPC ja "infekowałem" servletami, niepotrzebnie komplikując zadanie.

Dla zobrazowania tematu przedstawię sposób komunikacji aplikacji GWT (wykonywanej w przeglądarce jako JavaScript) z częścią serwerową reprezentowaną przez servlet (wykonywanej na serwerze w dowolnej technologii, m.in. Java EE):

RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, "/iloscGraczy");
try {
builder.sendRequest(null, new RequestCallback() {
public void onError(Request request, Throwable exception) {
Window.alert(exception.toString());
}

public void onResponseReceived(Request request, Response response) {
WitajSwiecieGWT.this.iloscGraczy = Integer.parseInt(response.getText());
}
});
} catch (RequestException e) {
Window.alert("Failed to send the request: " + e.getMessage());
}

i przez zdalną usługę w "czystym" GWT RPC:

AkcjaAsync remoteService = Akcja.App.getInstance();
remoteService.zarejestrujGracza(new AsyncCallback() {
public void onSuccess(Object result) {
label.setText("Zarejestrowano nowego gracza");
}

public void onFailure(Throwable caught) {
Window.alert(caught.toString());
}
});

Niby to samo, ale zmienia się podejście do zdalnej usługi, która jest źródłem danych. Niepotrzebnie wprowadzałem kolejną technologię - servlet - do zrealizowania funkcjonalności, którą ostatecznie obsłużyłem jako zdalną usługę w GWT RPC. Oczywiście w sytuacjach migracji z jednej technologii klienckiej (wliczam w to również JSF) do GWT taka sytuacja może się zdarzyć, ale przy tworzeniu aplikacji "od zera" wydaje się być niepotrzebne.

W zasadzie zrozumienie GWT RPC jest kluczem do stworzenia wyrafinowanej aplikacji w GWT. Zakładam jednocześnie, że samo poznanie kontrolek graficznych GWT - przyciski, okna, etc. - jest nieodzownym elementem poznawania GWT, ale przy znajomości JFC/Swing możemy ten krok pominąć (zakładając, że to już się po prostu zna). Podobieństwo między GWT a Swing jest tak ogromne, że możnaby postawić pomiędzy nimi znak równości.

Skoro GWT RPC jest tak istotne, to może istnieje sposób na skorzystanie z niego nie tracąc wiele czasu na zrozumienie istoty jego działania? Sądzę, że istnieje i już po samym zapoznaniu się z dokumentacją GWT - Remote Procedure Calls wszystko powinno stać się jasne. O GWT RPC pisałem również poprzednio - GWT RPC - mechanizm zdalnego wywoływania procedur w GWT w Notatniku, więc w zasadzie większość, jeśli nie cała, teoria za GWT RPC została już zaprezentowana. Najtrudniej jest przestawić się z myślenia technologiami klienckimi uruchamianymi na serwerze (wspierającymi tworzenie interfejsu użytkownika, np. JSF) na technologię kliencką uruchamianą w przeglądarce tworzoną w...Javie, czyli GWT. GWT RPC jest na tyle istotne, że warto poświęcić mu ponownie kilka chwil.

Procedura tworzenia zdalnej usługi w GWT RPC.

Krok 1. Utworzenie interfejsu usługi Gra

Hmmm, brzmi jak pierwszy krok podczas tworzenia komponentów EJB 3.0, czy w ogóle, dowolnej, modularnej aplikacji. To się może podobać każdemu (!)

package pl.jaceklaskowski.gwt.kolkoikrzyzyk.client;

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

public interface Gra extends RemoteService {
/**
* Zarejestruj gracza
*
* @return true jesli gracz zostal zarejestrowany, wpp false
*/
public boolean zarejestrujGracza();
}

Uwaga 1: Część kliencka musi być w pakiecie client będącymi podpakietem dowolnego pakietu.

Uwaga 2: Interfejs usługi musi rozszerzać interfejs com.google.gwt.user.client.rpc.RemoteService.

Krok 2. Utworzenie interfejsu asynchronicznego GraAsync

To niestety zaczyna pachnieć nieświeżo - jak programowanie z EJB 2.1 i wersjami poprzednimi. Chciałoby się skorzystać z adnotacji, które w aktualnej wersji GWT 1.3.3 są niemożliwe ze względu na ograniczenia wspieranej wersji języka Java - 1.4.2 i niżej. Przeżyliśmy EJB 2.1, przeżyjemy i GWT 1.3.3.

package pl.jaceklaskowski.gwt.kolkoikrzyzyk.client;

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

public interface GraAsync {
/**
* Zarejestruj gracza
*
* @return true jesli gracz zostal zarejestrowany, wpp false
*/
void zarejestrujGracza(AsyncCallback async);
}

Uwaga 1: Interfejs asynchroniczny usługi musi być nazwany zgodnie z regułą - nazwa interfejsu usługi + Async.

Uwaga 2: Każda metoda interfejsu asynchronicznego musi składać się z dodatkowego, dodawanego na końcu listy parametrów wejściowych parametru o typie AsyncCallback.

Krok 3. Utworzenie implementacji usługi GraImpl

package pl.jaceklaskowski.gwt.kolkoikrzyzyk.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import pl.jaceklaskowski.gwt.kolkoikrzyzyk.client.Gra;

import javax.servlet.http.HttpServletRequest;

public class GraImpl extends RemoteServiceServlet implements Gra {
public boolean zarejestrujGracza() {
HttpServletRequest request = getThreadLocalRequest();
// pracuj z obiektem request
// wszystkie inne metody HttpServlet sa rowniez dostepne - jestemy w koncu w servlecie
return true;
}
}

Uwaga 1: Implementacja usługi musi znajdować się w odpowiednim podpakiecie server.

Uwaga 2: Implementacja jest wykonywana na serwerze i dowolna technologia może zostać użyta do jej utworzenia.

Uwaga 3: Implementacja musi rozszerzać interfejs usługi a nie interfejs asynchroniczny.

Uwaga 4: Dostęp do obiektu request i response możliwy poprzez metody RemoteServiceServlet.getThreadLocalRequest oraz RemoteServiceServlet.getThreadLocalResponse, odpowiednio.

Krok 4. Rejestracja usługi - KolkoIKrzyzyk.gwt.xml

Kolejny krok, który mógłby być zrealizowany przez adnotacje. W końcu czasy plików konfiguracyjnych w XML są daleko za nami, nieprawdaż?

<module>

<inherits name='com.google.gwt.user.User'/>

<entry-point class='pl.jaceklaskowski.gwt.kolkoikrzyzyk.client.KolkoiKrzyzykGWT'/>

<servlet path="/gra" class="pl.jaceklaskowski.gwt.kolkoikrzyzyk.server.GraImpl"/>
</module>

Uwaga 1: Rejestrujemy usługę zdalną za pomocą elementu servlet, którego atrybut path określa ścieżkę mapowania servletu, a class klasę implementacji usługi.

Krok 5. Wywołanie usługi

GraAsync graUslugaZdalna = (GraAsync) GWT.create(Gra.class);
ServiceDefTarget endpoint = (ServiceDefTarget) graUslugaZdalna;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL() + "gra");

AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
// result zawiera egzemplarz zwrócony podczas wywołania zdalnej metody
// tutaj wykonujemy operacje na interfejsie użytkownika
}

public void onFailure(Throwable caught) {
// bardzo przykładowa realizacja
Window.alert(caught.toString());
}
}

graUslugaZdalna.zarejestrujGracza(callback);

Uwaga 1: W GWT nie istnieje możliwość wykonania usługi synchronicznie, więc należy "przestawić się" na myślenie asynchroniczne, tj. dane mogą nadejść po pewnym czasie.

Uwaga 2: Adres servletu w linii 3. przykładu, gdzie wykonywana jest metoda setServiceEntryPoint odpowiada dokładnie ciągowi znaków wpisanemu w pliku konfiguracyjnym modułu w atrybucie path elementu servlet.

Uwaga 3: Warto skorzystać ze "wzorca", które podpowiada IntelliJ IDEA 6, tj. dodać poniższą klasę do interfejsu usługi

public static class App {
private static GraAsync ourInstance = null;

public static synchronized GraAsync getInstance() {
if (ourInstance == null) {
ourInstance = (GraAsync) GWT.create(Gra.class);
((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "gra");
}
return ourInstance;
}

co sprowadzi powyższy krok do następującego:

GraAsync graUslugaZdalna = Gra.App.getInstance();

AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
// result zawiera egzemplarz zwrócony podczas wywołania zdalnej metody
// tutaj wykonujemy operacje na interfejsie użytkownika
}

public void onFailure(Throwable caught) {
// bardzo przykładowa realizacja
Window.alert(caught.toString());
}
}

graUslugaZdalna.zarejestrujGracza(callback);

Krok 6. Integracja usługi z aplikacją

Samo wywołanie usługi należy podpiąć do wykonania zdarzenia w interfejsie użytkownika pamiętając, że dane napływają z opóźnieniem, tj. asynchronicznie.

Jest wiele ciekawych rozwiązań w GWT, ale poza GWT RPC do stworzenia mojej gry skorzystałem również z mechanizmu Timer. Zacznijmy od przykładu.

Timer t = new Timer() {
public void run() {
label.setText("Sprawdzam, czy zasiedli wszyscy gracze");
if (moznaRozpoczacGre()) {
label.setText("Wyłączam sprawdzanie - wszyscy gotowi, można zaczynać, zatem rozpoczynam grę");
cancel();
}
}
};
t.scheduleRepeating(5000); // uruchamiaj co 5 sekund

gdzie metoda moznaRozpoczacGre prezentuje się następująco:

private boolean moznaRozpoczacGre() {
// polacz sie z serwerem i sprawdz ilosc graczy
// wykonaj zdalną usługę i zwróć ilość graczy
label.setText("Ilość graczy: " + iloscGraczy);
return iloscGraczy == 2;
}

W przykładzie stworzyłem egzemplarz typu Timer i dostarczyłem własną realizację metody run, która będzie wykonywana cyklicznie, zgodnie z parametrem metody schedule lub scheduleRepeating. Różnica między nimi to czas trwania zegara i moment jego wywołania - w pierwszej zegar uruchomi się raz za zadany czas, podczas, gdy w drugim przypadku będzie wykonywał się do momentu wywołania metody cancel co zadany okres czasu. Jest to najbardziej zadowalające mnie rozwiązanie do symulacji ciągłej interakcji między klientem (przeglądarka) a serwerem.

Znajomość GWT RPC, Timer oraz przyzwyczajenie się do asynchronicznej natury GWT to klucz do tworzenia aplikacji z GWT. Reszta to szczegóły, w których trudno doszukiwać się diabła ;-) Pora na rekonesans po JPA!