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 GraHmmm, 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 GraAsyncTo 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.xmlKolejny 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!