02 maja 2007

GWT RPC - mechanizm zdalnego wywoływania procedur w GWT

Kontynuuję rozpoznawanie GWT i do mojej pierwszej aplikacji korzystającej z Java EE i GWT pozostaje poznać mechanizm spinający świat technologii klienckich - uruchamianych w przeglądarce - z serwerowymi. Na scenę wchodzi - GWT RPC. Z nim czuję, że jestem bliżej celu.

Jak wspomniałem wcześniej budowanie aplikacji GWT to programowanie w Javie stron będących mieszanką technologii klienckich (z punktu widzenia architektur wielowarstowych Java EE, w których klientem jest przeglądarka) - HTML, JavaScript oraz Ajax (przez co rozumiem - przynajmniej obecnie - zarządzanie obiektem XMLHttpRequest oraz modyfikacją DOM).

Największym wyzwaniem dla programistów Java pracującymi z technologiami serwerowymi wspierającymi dynamiczne konstruowanie interfejsu użytkownika - JSF, JSP, Servlety i in. - jest zrozumienie końcowego wyniku tworzenia aplikacji GWT - utworzenie strony HTML z "dodatkami" nie będącymi w żaden sposób związanymi z Javą (poza jej rolą jako język do ich utworzenia).

Dla uproszczenia zrozumienia roli GWT w tworzeniu aplikacji internetowej (klient = przeglądarka) wyróżnijmy etapy w jej życiu (tylko te, które są wartościowe w naszej dywagacji o GWT):
  1. Programowanie - etap, w którym programista pisze kod źródłowy w Javie.
  2. Kompilacja - etap, w którym kod źródłowy jest zmieniany na postać akceptowaną przez środowisko uruchomieniowe (bajtkod lub HTML).
  3. Uruchomienie - etap, w którym bajtkod produkuje treść wysyłaną do przeglądarki.
Co łączy tworzenie aplikacji w Java EE i GWT to fakt, że są one dedykowane dla środowiska uruchomieniowego, w którym główną rolę odgrywa język Java. Kod źródłowy tworzony jest w Javie i to jest jedyna część wspólna. Pozostałe etapy są już inne. I tutaj właśnie tkwi problem w moim wcześniejszym zrozumieniu GWT. W jego przypadku, postacią uruchomieniową kodu źródłowego jest HTML podczas, gdy w technologiach Java EE jest to bajtkod (bo tak kończy program napisany w Javie). Oczywiście w efekcie i GWT i Java EE "produkuje" HTML, ale różnica tkwi w momencie jego dostępności. W przypadku GWT, HTML pojawia się już w kroku 2., podczas gdy w Java EE jest materializowany dopiero w etapie 3. - uruchomieniowym.

Pamiętając o różnicach między GWT a Java EE, tworzenie aplikacji będących mieszanką ich obu nie powinno stanowić problemu. Jak HTML (będący postacią wynikową kompilacji aplikacji GWT) mógłby zostać wkomponowany w aplikację Java EE każdy wie. Możemy zmodyfikować HTML na stronę JSP, która z kolei korzysta z innych technologii, jednakże ostatecznie struktura wynikowa strony przesyłanej do przeglądarki musi spełniać wymagania GWT. Nie przekreśla to możliwości skorzystania z innych rozwiązań, np. JSF (choć na chwilę obecną nie wiem jak miałoby to wyglądać praktycznie, a jedynie teoretycznie).

Kiedy tworzymy aplikację GWT mamy do dyspozycji kilka elementów składowych podczas jej tworzenia (etap programowanie), które były już przedstawiane poprzednio, jednakże warto o nich wspomnieć ponownie:
  • entry-point - punkt dostępowy - klasa realizująca interfejs EntryPoint, która docelowo stanie się stroną HTML. Jest to część kliencka GWT. Może istnieć wiele punktów dostępowych.
  • servlet - klasa rozszerzająca klasę RemoteServiceServlet, która jest definicją servletu - części aplikacji wykonywanej po stronie serwera (w sensie GWT i Java EE) i wywoływanej przez mechanizm GWT RPC. Klasa, z której dziedziczy servlet jest jedynie klasą pochodną znanej z Java EE klasy javax.servlet.http.HttpServlet i obsługuje mechanizm serializacji.
Wspomniany mechanizm zdalnego wywoływania procedur GWT (ang. GWT Remote Procedure Call lub w skrócie GWT RPC) to sposób na zdalne wywoływanie części serwerowej aplikacji w GWT. Podobnie jak tradycyjna aplikacja desktopowa ma możliwość pozyskiwania danych z różnych zewnętrznych źródeł, tak i aplikacja GWT ma taką możliwość. Nie jest to do końca równoważne, gdyż aplikacja GWT działa w ramach przeglądarki i obowiązuje ją zasada łączenia się jedynie z serwerem, z którego pochodzi, jednakże sama koncepcja pozyskiwania danych jest analogiczna. Sposób działania aplikacji GWT jest łudząco podobny do działania tradycyjnej aplikacji desktopowej. Za pomocą GWT RPC mamy możliwość przekazywania obiektów do/z serwera poprzez HTTP - proces serializacji. W skrócie wykorzystanie mechanizmu RPC w aplikacji sprowadza się do definicji servletu w pliku konfiguracyjnym aplikacji, definicji oraz realizacji interfejsu i ostatecznie na wywołaniu usługi z aplikacji.

Pora na krótką demonstrację teorii w praktyce, co powinno znacząco uprościć zrozumienie tematu. Stworzę aplikację WitajSwiecieGWT.

Definiujemy zdalną usługę - część serwerową aplikacji - w deskryptorze modułu - WitajSwiecieGWT.gwt.xml za pomocą elementu servlet.

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

<entry-point class='pl.jaceklaskowski.gwt.witajswiecie.client.WitajSwiecieGWT'/>

<servlet path="/uslugaZdalna" class="pl.jaceklaskowski.gwt.witajswiecie.server.UslugaZdalnaImpl"/>
</module>

W punkcie dostępowym aplikacji - element entry-point - wywołana zostaje usługa zdalna jako wynik wciśnięcia przycisku.

package pl.jaceklaskowski.gwt.witajswiecie.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.*;
import com.google.gwt.user.client.rpc.AsyncCallback;

public class WitajSwiecieGWT implements EntryPoint {
public void onModuleLoad() {

final Button b = new Button("Przycisnij");
b.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
UslugaZdalnaAsync remoteService = UslugaZdalna.App.getInstance();
remoteService.wykonajZadanieNaSerwerze("Jacek", "Laskowski", new AsyncCallback() {
public void onSuccess(Object result) {
b.setText("Zakonczono poprawnie - wynik: " + result);
}

public void onFailure(Throwable caught) {
b.setText("Zakonczono niepoprawnie");
caught.printStackTrace();
}
});
}
});
RootPanel.get().add(b);
}
}

Klasa reprezentująca zdalną usługę - UsługaZdalna - to jedynie definicja interfejsu (w przykładzie mamy do dyspozycji pojedyńczą metodę wykonajZadanieNaSerwerze).

Dla usprawnienia programowania z GWT IntelliJ IDEA dostarcza metodę pomocniczą do tworzenia egzemplarzy UslugaZdalna.

package pl.jaceklaskowski.gwt.witajswiecie.client;

import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.core.client.GWT;

public interface UslugaZdalna extends RemoteService {

public String wykonajZadanieNaSerwerze(String imie, String nazwisko);

/**
* Metoda pomocnicza utworzona przez IntelliJ IDEA
* Use UslugaZdalna.App.getInstance() to access static instance of NowyRemoteServiceAsync
*/
public static class App {
private static UslugaZdalnaAsync ourInstance = null;

public static synchronized UslugaZdalnaAsync getInstance() {
if (ourInstance == null) {
ourInstance = (UslugaZdalnaAsync) GWT.create(UslugaZdalna.class);
((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "uslugaZdalna");
}
return ourInstance;
}
}
}

Zgodnie z wymaganiem GWT, tworzony egzemplarz musi być rzutowany na typ UslugaZdalnaAsync.

package pl.jaceklaskowski.gwt.witajswiecie.client;

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

public interface UslugaZdalnaAsync {
void wykonajZadanieNaSerwerze(String imie, String nazwisko, AsyncCallback async);
}

Aż w końcu należałoby zapoznać się z implementacją interfejsów w postaci usługi zdalnej wykonywanej na serwerze - klasa UslugaZdalnaImpl. Tutaj jedynym ograniczeniem jest nasza wyobraźnia. Wszystko wywoływane w ramach klasy jest wykonywane na serwerze i nie podlega obsłudze GWT. To jest miejsce pozyskiwania danych, np. poprzez JPA.

package pl.jaceklaskowski.gwt.witajswiecie.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import pl.jaceklaskowski.gwt.witajswiecie.client.UslugaZdalna;

public class UslugaZdalnaImpl extends RemoteServiceServlet implements UslugaZdalna {
public String wykonajZadanieNaSerwerze(String imie, String nazwisko) {
System.out.println("UslugaZdalnaImpl.wykonajZadanieNaSerwerze");

// Tutaj możnaby oczekiwać bardziej wyrafinowanego przetwarzania
return imie + " " + nazwisko;
}
}

Ważne, aby pamiętać, że mimo ograniczenia na konstrukcje językowe Java w GWT w części klienckiej (które muszą odpowiadać tym w Java SE 1.4 i wcześniejszym), część serwerowa nie leży w gestii zainteresowania GWT (poza udostępnieniem mechanizmu RPC) i może być tworzona z dowolnymi mechanizmami i technologiami Java SE czy EE.

Tworzenie usług zdalnych w GWT przypomina tworzenie komponentów EJB w wersji 2.1 i poprzednich, gdzie należało zdefiniować coś na kształt interfejsu biznesowego, interfejs domowy i właściwą klasę komponentu wszystko spięte za pomocą deskryptora ejb-jar.xml. Brakuje usprawnień, do których zdążyłem sie już przyzwyczaić pracując z Java EE 5, m.in. tworzenie komponentów (czy usług) w tradycyjny sposób - interfejs oraz klasa realizująca z adnotacjami. Może nadchodząca wersja GWT 2.0 będzie sprytniejsza?!

Na zakończenie strona HTML uruchamiająca GWT.

<html>
<head>
<title>Aplikacja WitajSwiecieGWT</title>
<meta name='gwt:module' content='pl.jaceklaskowski.gwt.witajswiecie.WitajSwiecieGWT'>
<link rel=stylesheet href="WitajSwiecieGWT.css">
</head>
<body>
<script language="javascript" src="gwt.js"></script>
<iframe id="__gwt_historyFrame" style="width:0;height:0;border:0"></iframe>
<h1>Aplikacja WitajSwiecieGWT</h1>
</body>
</html>

Cały moduł gotowy do uruchomienia można pobrać jako WitajSwiecieGWT.zip. Wymagane jest jedynie zainstalowanie GWT (i posiadanie MS Windows do uruchomienia skryptów ;-)).

Na zakończenie, ważna uwaga w kontekście tworzenia aplikacji GWT w środowisku IntelliJ IDEA 6.0 - Enable "before launching" steps. Podczas uruchamiania projektu należy włączyć tę opcję, gdyż w przeciwnym przypadku część serwerowa nie zostanie zbudowana i uruchomienie mechanizmów GWT RPC zakończy się komunikatem błędu, który może prezentować się w ten sposób:


co wynika wyłącznie z niedostępności skompilowanej klasy na ścieżce klas.

Przy okazji rozwiązywania problemu (co okazało się być wyłącznie związane z samym posługiwaniem się IDEA) zauważyłem, że próbowałem korzystać z tradycyjnych metod tworząc aplikację GWT, np. caught.printStackTrace(), czy pomysłami w stylu instalacja Java SE 1.4 (co szczęśliwie zakończyło się przypomnieniem, że Java SE 5 ma możliwość kompilacji i uruchomienia z obniżoną wersją języka). Zauważam, że GWT wymaga pewnego oderwania się od dotychczasowych przyzwyczajeń i to, co było dobre w innych serwerowych technologiach tu może nie być najwłaściwszym podejściem. Ważne, aby podczas programowania z GWT korzystać z narzędzi, które są oferowane z GWT - GWT Development Shell z opcją śledzenia wykonywania programów (debug) czy podświetlenia linii z błędem, która zostanie uzupełniona o informacje pomocne w rozwiązywaniu aktualnego problemu (jak widać na powyższym zrzucie ekranu). Niestety nad rozwiązaniem tego (a przy okazji i zdobywaniem kolejnych informacji o GWT) spędziłem bodajże około 2 dni (!) Czasami warto zapomnieć o tym co się wie i zacząć wszystko od nowa, bo doświadczenie to często niepotrzebny bagaż (doświadczyłem tego na własnej skórze, kiedy uczyłem się, początkowo nieosiągalnej, sztuki żonglowania).