JAVA exPress > Archive > Issue 3 (2009-03-08) > GWT dla początkujących

GWT dla początkujących

Na początku wypadałoby wyjaśnić co to takiego Google Web Toolkit. Bardzo ogólnie można by powiedzieć, że jest to kompilator, potrafiący przetworzyć (skompilować) kod Javy na JavaScript, mający też kilka innych ułatwiających życie funkcji. Dzięki temu pozwala na pisanie aplikacji webowych działających po stronie klienta, bez znajomości JavaScriptu. Oprócz tego daje nam kilka ułatwień, których nie mielibyśmy programując w czystym JavaScript. Jednym z nich jest obsługa różnych wersji JavaScriptu - niestety występują w nich różnice na różnych przeglądarkach. Poza tym łatwo tworzy się serwisy RPC (Remote Procedure Call), pozwalające aplikacji działającej po stronie klienta na komunikację z serwerem. Jeśli w Twojej głowie wyświetliło się słowo "AJAX", to bardzo słusznie. GWT to nic innego jak kolejne narzędzie do tworzenia aplikacji AJAXowych, jednak istotnie różniące się od "konkurencji".

Prosta aplikacja kliencka

GWT dostarczane jest z narzędziami do tworzenia projektu. Tworzą one potrzebną strukturę katalogów i podstawowe pliki. Można też stworzyć projekt gotowy do zaimportowania w Eclipsie. Polecenie:

        projectCreator -eclipse java-express -out java-express
        

Stworzy pusty projekt eclipse'a.

Polecenie

        applicationCreator -out java-express -eclipse java-express
        pl.dworld.javaexpress.gwt.client.JavaExpress
        

Utworzy przykładową aplikację, ze skryptami do kompilowania i uruchamiania. Modyfikator -eclipse java-express utworzy jeszcze konfigurację odpalania (plik *.launch) dla Eclipse'a.

Oba powyższe polecenia (zastosowane jedno po drugim) utworzą projekt z przykładową aplikacją, który będzie można zaimportować do Eclipse'a i uruchomić. Odpalenie w Eclipse aplikacji z zawartego w projekcie pliku *.launch uruchomi tak zwanego shella. Shell to takie GWTowe środowisko uruchamiania ze specjalną przeglądarką. Jest bardzo przydatne przy developmencie, testowaniu i debugowaniu. Jeśli mamy błąd w kodzie klienckim, po uruchomieniu skompilowanego kodu w przeglądarce dostaniemy tylko enigmatyczny komunikat błędu z JavaScriptu. W GWT Shellu dostaniemy pełen stack trace z Javy.

Rys 1. Po lewej stronie GWT Shell. Po prawej „shellowa” przeglądarka.

W tym momencie kod kliencki nie jest jeszcze skompilowany do JavaScriptu, można go skompilować wciskając przycisk "Compile/Browse". Teraz możemy już otworzyć naszą aplikację w dowolnej przeglądarce.

Co my tu mamy - czyli co jest w wygenerowanej aplikacji

W wygenerowanej aplikacji istotny jest pakiet client. W pakiecie tym mamy już przykładową klasę. Innych pakietów (pod pakietem pl.dworld.javaexpress.gwt) na razie nie mamy. Jeśli nasza aplikacja ma działać całkowicie po stronie klienta (np. prosty kalkulator) i nie potrzebuje komunikować się z serwerem, może tak pozostać. Jeśli jednak chcielibyśmy zrobić aplikację, która będzie wysyłała na serwer jakieś dane lub je pobierała, klasy z którymi klient będzie się komunikował muszą być umieszczone poza pakietem client.

Spójrzmy teraz na naszą prostą aplikację. Nie licząc plików konfiguracyjnych serwera, składa się ona z jednej klasy, jednej strony HTML i pliku modułu GWT (JavaExpress.gwt.xml). W module określony jest EntryPoint, czyli klasa, która ma "obsłużyć" ładowaną stronę html. O wszystkich klasach z pakietu client możemy myśleć jak o plikach JavaScript, tzn. są one wykonywane po stronie klienta, przez przeglądarkę. Rzeczywiście, w momencie budowania aplikacji GWT, kod Javy znajdujący się w tym pakiecie jest kompilowany do JavaScriptu. W klasie implementującej EntryPoint znajduje się następujący kod:

 
        RootPanel.get().add(button);
        RootPanel.get().add(label);
 

Te dwie linie dodają przycisk i etykietę do strony HTML. Zazwyczaj aplikacje GWT to jedna strona, początkowo praktycznie pusta, wypełniana potem dynamicznie przez JavaScript. Pewenym problemem w takiej sytuacji stają się zakładki do stron (bookmarks), jednak i na to jest sposób, napiszę o nim nieco później.

Komunikacja z serwerem

Mamy zatem gotową aplikację, która działa całkiem po stronie klienta. Wszystko jest w JavaScripcie, serwer jest potrzebny tylko do dostarczenia JavaScriptu przeglądarce użytkownika. Nie jest to jeszcze AJAX, nie mamy tu żadnych odwołań do serwera, których wynik (dane) byłby dynamicznie prezentowany na stronie w momencie jego otrzymania przez klienta, bez potrzeby przeładowania całej strony. Oczywiście, w GWT można się do serwera odwołać. Aby to zrobić trzeba utworzyć kilka interfejsów i klas, zachowując określoną konwencję. Zanim to opiszę, krótkie wyjaśnienie co to są wywołania synchroniczne i asynchroniczne. Wywołania synchroniczne to takie, w których program nie wykonuje się dalej dopóki nie dostanie wyniku wywołania. Każde wywołanie zwykłej metody w Javie jest wywołaniem synchronicznym, np.

 
        int a = getValue();
        System.out.println("value is here")
 

jest wywołaniem synchronicznym. Druga linia nie wykona się dopóki nie zakończy się wykonywanie pierwszej. W aplikacjach AJAXowych natomiast mamy do czynienia z wywołaniami asynchronicznymi, np.

 
        getValue(new AsyncCallback() {
          public void onSuccess(int result) {
            a = result;
          }
        });
        System.out.println("value is not here");
 

Wywołana jest metoda getValue(...), i zanim jeszcze wynik tego wywołania będzie znany, wykona się linia System.out.println("value is not here"); Wartość wywołania zostanie przypisana do zmiennej a w momencie kiedy będzie znana, może to być po kilku milisekundach, może być po kilku sekundach - nie ma to znaczenia. Nasz program wykonuje się dalej (i np. reaguje na działania użytkownika). Dobrym przykładem takiej AJAXowej aplikacji jest http://maps.google.com. Jak przesuwamy mapę, poszczególne jej fragmenty się ładują, ale zanim jeszcze się załadują, nie jesteśmy "zablokowani", możemy dalej przesuwać mapę. W momencie załadowania danego fragmentu zostanie on wyświetlony.

A jak to się robi w GWT? Zaczniemy od synchronicznego interfejsu serwisu w pakiecie client:

 
        public interface GwtService extends RemoteService {
            String getData(String s);
        }
 

Warto tu nadmienić jeszcze jedno. Część serwerowa "widzi" klasy części klienckiej, dla serwera cała aplikacja to po prostu klasy Javy. Część kliencka jednak nie widzi serwerowej, i jest to logiczne, po skompilowaniu do JavaScriptu, kod JavaScript, wykonywany w przeglądarce, nie może mieć dostępu do klas Javy. Dlatego jeśli chcecie przekazywać między serwerem a klientem jakieś własne obiekty, muszą one być umieszczone w pakiecie client.

Ok, mamy interfejs synchroniczny, zróbmy zatem jego implementację. W zasadzie każdy kod umieszczony poza pakietem client jest traktowany jako kod serwerowy. Na potrzeby tego artykułu utworzymy dla niego pakiet pl.dworld.javaexpress.gwt.server. W nim też umieścimy poniższą implementację:

 
        public class GwtServiceImpl extends RemoteServiceServlet implements GwtService {
            public String getData(String s) {
                return getDataFromDb(s);
            }
 
            private String getDataFromDb(String s) {
                return s.toUpperCase() + " " + System.currentTimeMillis();
            }
        }
 

Mamy interfejs, mamy implementację, czyli wszystko czego potrzeba po stronie serwerowej. Trzeba ją jeszcze "wystawić" tak, aby była dostępna dla klienta. Nasza implementacja GwtServiceImpl to nic innego jak servlet, odpowiadający na żądania POST. Jak zapewne zauważyłeś, drogi czytelniku, metoda getDataFromDb tak naprawdę nie pobiera niczego z bazy danych, na potrzeby tego artykułu jednak taki "fake" wystarczy. Wystawiamy dopisując do pliku JavaExpress.gwt.xml następującą linię:

 
        <servlet path="/service" class="pl.dworld.javaexpress.gwt.server.GwtServiceImpl" />
 

To spowoduje wystawienie naszego servletu pod urlem o końcówce "/service".

Ale zaraz zaraz, przecież miało być asynchronicznie? I po stronie klienta tak będzie. Wszystkie pozostałe klasy będziemy tworzyć w pakiecie client. Potrzebny będzie asynchroniczny interfejs:

 
        public interface GwtServiceAsync {
            public void getData(String s, AsyncCallback<String> callback);
        }
 

Interfejs ten nieznacznie różni się od wersji synchronicznej:

- Nazwa taka sama z dodanym słówkiem Async

- Każda metoda typu void

- Do każdej metody dodany parametr typu AsyncCallback

- Typ zwracany z metody interfejsu synchronicznego trafia do callback'u.

Ok, mamy już wszystko czego nam potrzeba. Teraz zobaczmy jak się tego używa. Napiszemy sobie prosty widget do tego celu. Widget to część GUI, coś co można wstawić na stronę GWT. Oczywiście musi być w pakiecie client. Na początek bez części odwołującej się do serwera, za chwilę ją dodamy.

 
        public class GwtServiceUser extends Composite {
            private VerticalPanel m_vPanel = new VerticalPanel();
            private Label m_label = new Label();
            private TextBox m_textBox = new TextBox();
 
            public GwtServiceUser() {
                m_vPanel.add(m_textBox);
                m_vPanel.add(new Button("Use service"));
                m_vPanel.add(m_label);
                initWidget(m_vPanel);
            }
        }
 

Ok, dodajmy teraz ten widget do naszej strony. Nic prostszego, w klasie JavaExpress dodajemy na końcu:

 
        RootPanel.get().add(new GwtServiceUser());
 

Dobra, pora naprawdę zapytać o coś serwer. Potrzebny do tego będzie obiekt implementujący interfejs GwtServiceAsync. Przekażemy go do naszego widgetu przez konstruktor, który teraz będzie wyglądał tak:

 
            public GwtServiceUser(GwtServiceAsync service) {
                m_vPanel.add(new Button("Use service"));
                m_vPanel.add(m_label);
                initWidget(m_vPanel);
            }
 

Skąd go wziąć? Utworzymy go jedną linią kodu, więc teraz ostatnie linie klasy JavaExpress będą wyglądać tak:

 
        GwtServiceAsync service = (GwtServiceAsync)
        GWT.create(GwtService.class);
        RootPanel.get().add(new GwtServiceUser(service));
 

Istotna jest jeszcze adnotacja

 
        @RemoteServiceRelativePath("/service")
 

Możemy ją dodać bezpośrednio przed wywołaniem GWT.create, możemy też (i tak będzie w tym przypadku) przed interfejsem GwtService. Będzie on więc wyglądał tak:

 
        @RemoteServiceRelativePath("/service")
        public interface GwtService extends RemoteService {
 

W GWT.create(...) zawiera się cała "magia" GWT. To właśnie ta metoda, korzystając z naszych interfejsów (asynchronicznego i synchronicznego) i adnotacji, utworzy nam implementację GwtServiceAsync, odwołującą się do servletu znajdującego się pod urlem "/service". Pełen kod widgetu korzystającego z tego serwisu wygląda tak:

 
        public class GwtServiceUser extends Composite {
          private VerticalPanel m_vPanel = new VerticalPanel();
          private Label m_label = new Label();
          private TextBox m_textBox = new TextBox();
          public GwtServiceUser(final GwtServiceAsync service) {
             m_vPanel.add(m_textBox);
            m_vPanel.add(new Button("Use service", new ClickListener() {
              public void onClick(Widget sender) {
                service.getData(m_textBox.getText(), new AsyncCallback<String>(){
                  public void onFailure(Throwable caught) {
                    Window.alert("Error!");
                  }
                  public void onSuccess(String result) {
                    m_label.setText(result);
                  }
                });
              }
            }));
            m_vPanel.add(m_label);
            initWidget(m_vPanel);
          }
        }
 

Wywołanie serwisu RPC opiera się na tzw. callbackach. Interface AsyncCallback ma dwie metody: onFailure i onSuccess. Ich nazwy nie wymagają chyba wyjaśnień. W przypadku powodzenia wywołania RPC, jego wynik trafia do metody onSuccess i tam też musimy go obsłużyć. Ważne jest, żeby pamiętać że jest to wywołanie asynchroniczne i reszta kodu klienckiego może się wykonywać w czasie oczekiwania na rezultat tego wywołania. Dość częstym błędem jest przepisywanie rezultatu wywołania do jakiejś zmiennej klasy i używanie jej w kodzie następującym bezpośrednio po wywołaniu, czyli w momencie, gdy jeszcze rezultat wywołania nie jest znany (zmienna nie jest zainicjalizowana).

Tworzenie interfejsu użytkownika

Interfejs użytkownika w GWT opiera sie na tzw. widgetach. Są to elementy takie jak pola tekstowe, przyciski, napisy itp. Specjalnym rodzajem widgetów są panele. Panele służą do układania widgetów (także innych paneli) na stronie. Robi się to podobnie jak w Swingu. Panele są odpowiednikiem Swingowych layoutów, aczkolwiek zrealizowanym nieco inaczej. I tak jest np. VerticalPanel, na którym komponenty są jeden pod drugim, jest HorizontalPanel, jest FlowPanel, w którym komponenty umieszczane są poziomo, a jak miejsce się kończy, to w kolenym rzędzie pod spodem. Odpowiednikiem BorderLayout ze Swinga jest DockPanel. Jest także TabPanel, pokazujący zawartość w zakładkach. Bardziej zaawansowane przykłady to Grid i FlexTable.

Tworzenie własnych widgetów

Klasa GwtServiceUser, którą właśnie utworzyliśmy, to nic innego jak nasz własny widget. Jak widać rozszerza on klasę Composite. Jest to standardowy sposób pisania własnych widgetów dla GWT. Jedynym warunkiem jest wywołanie metody initWidget() w konstruktorze. W metodzie tej podajemy m_vPanel, czyli panel pionowy z przyciskiem i napisem. W ten sposób wstawiając na stronę GwtServiceUser wstawiamy od razu napis i przycisk, odpowiednio już oprogramowany.

Nie wszystko złoto co się świeci

Nie każdy kod Javy da się skompilować do JavaScriptu za pomocą GWT. Kompilowalne są prymitywy i podstawowe klasy i interface'y, takie jak Collection, List, Map (i ich standardowe implementacje), popularne wyjątki i klasy użytkowe (StringBuffer, Math itp.). Pełna lista jest pod adresem http://code.google.com/docreader/#t=RefJreEmulation

Oczywiście można pisać własne klasy kompilowalne do JavaScriptu, wystarczy tylko żeby wszystko czego będziemy w nich używać (pola, zmienne) też było kompilowalne.

Może się to wydawać mało, ale naprawdę da się wykorzystując tylko ten zestaw klas pisać zaawansowane aplikacje w GWT. Poza tym trzeba pamiętać, że ograniczenie to dotyczy tylko kodu części klienckiej, po stronie serwera możemy używać wszystkich dostępnych klas.

Zakładki i przyciski "Back" i "Forward"

Pewnym problemem w GWT są zakładki (bookmarks). Skoro wszystko dzieje się na jednej stronie, a zmienia się tylko dynamicznie jej zawartość, to cała aplikacja ma jeden adres. Trudno w takiej sytuacji zrobić zakładkę do strony innej niż startowa. Jednak i na to jest sposób. Po URLu w przeglądarce można dodać dowolny tekst po znaku "#" (tzw. token). Nie jest to już część adresu. Można więc tworzyć linki i przyciski "przekierowujące" na tę samą stronę, tylko z innym tekstem po znaku "#". W GWT jest klasa History, do której możemy podłączyć HistoryListener. W ten sposób jesteśmy informowani o wszelkich zmianach URLa (a więc także tokena) w przeglądarce, także tych wywołanych przez wciskanie przycisków Back i Forward. Za pomocą klasy History możemy też programowo odkładać na stos historii nowe adresy, więc możemy to zrobić w dowolnym momencie, nie tylko przekierowując na taką stronę. Możemy na przykład dołożyć element na stos historii w momencie kiedy użytkownik kliknie przycisk na stronie.

Moduły

Aplikacje GWT składają się z modułów. W prostych zastosowaniach (a właściwie często w skomplikowanych też) wystarczy jeden moduł. Moduł GWT to taka GWTowa biblioteka. Można go spakować do jara i używać w GWTowych projektach. Może mieć zarówno kod kliencki jak i serwerowy. Jednak jeśli zawiera kod kliencki, musi zawierać jego źródła. Pamiętajmy, że GWT kompiluje źródła Javy do JavaScriptu. Źródła, nie bytecode. Każdy moduł ma swoją konfigurację. W naszej prostej aplikacji znajduje się ona w pliku JavaExpress.gwt.xml.

Moduły mogą po sobie dziedziczyć. W naszej aplikacji w pliku JavaExpress.gwt.xml znajdziemy linię <inherits name="com.google.gwt.user.User" />. Oznacza to, że nasz moduł ma też funkcjinalność modułu com.google.gwt.user.User. Jest to moduł GWT dołączany standardowo do każdej aplikacji. Z kolei w module com.google.gwt.user.User znaleźlibyśmy linię <inherits name="com.google.gwt.core.Core" />, więc w naszm module niejawnie mamy także funkcjonalność z modułu Core.

W module określony jest także tzw. EntryPoint. Jest to klasa, która obsługuje daną stronę. To tam znajduje się kod wyszukujący na stronie odpowiednie miejsce i modyfikujący w tym miejscu drzewo DOM dokumentu HTML.

GWT ma tę zaletę, że opiera się na pewnych standardach. Np. kod kliencki, ten kompilowany do JavaScriptu, standardowo znajduje się w pakiecie client. Możemy w pliku xml modułu skonfigurować inną lokację, ale nie musimy tego robić jeśli standard jest zachowany. Podobnie rzecz się ma z plikami "publicznymi". Są to wszystkie pliki które serwer udostępnia przeglądarce. Możemy tam wrzucić podstrony w HTMLu, obrazki, pliki css itd. Domyślnie powinny się one znajdować w katalogu public. Jeśli chcemy trzymać te pliki w innym katalogu - proszę bardzo, wystarczy tylko skonfigurować odpowiednio moduł.

Kolejną istotną rzeczą w module są servlety. Przykład mamy już w naszej aplikacji:

 
        <servlet path="/service" class="pl.dworld.javaexpress.gwt.server.GwtServiceImpl" />
 

Oznacza to, że serwis GWT zaimplementowany w klasie pl.dworld.javaexpress.gwt.server.GwtServiceImpl będzie wystawiony pod urlem "/service". Oczywiście możemy wystawiać wiele serwisów.

W module możemy też zdefiniować pliki CSS i JavaScript, które chcemy dodać do strony.

Istotną rzeczą o której należy pamiętać jest fakt, że servlety skonfigurowane w pliku modułu są brane pod uwagę tylko przy uruchamianiu aplikacji GWT z tzw. shella. To znaczy, że jeśli już skompilujemy naszą Javę do JavaScriptu i będziemy chcieli zdeployować na jakimś servlet containerze (np. Tomcacie), to te wszystkie servlety będziemy musieli wpisać do pliku web.xml.

Testowanie GWTTestCase

Jeśli piszemy aplikację po stronie klienta serwerowi zostawiając tylko to, co rzeczywiście MUSI być po stronie serwera, to dużą część (jeśli nie całość) logiki biznesowej też będziemy mieli po stronie klienta. Warto dla niej napisać testy. Nie ma problemu jeśli są to "zwykłe" klasy, nie korzystające z mechanizmów specyficznych dla GWT (np. GWT.create()). Wtedy możemy napisać "normalny" test JUnit. Jeśli jednak nasze klasy korzystają z takich mechanizmów, możemy wykorzystać GWTTestCase. Klasa ta dziedziczy po klasie TestCase z JUnit 3, zatem nie możemy wykorzystać adnotacji @Test, @Before itd. Nasze metody testowe muszą być publiczne i typu void, a ich nazwy muszą zaczynać się od "test". Ponieważ klasa GWTTestCase nadpisuje metody setUp oraz tearDown i przygotowuje w nich test GWT oraz "sprząta" po nim, nie powinniśmy ich nadpisywać. Twórcy GWT przygotowali dla nas analogiczne metody gwtSetUp i gwtTearDown. Kod wykonywany w metodach testowych w GWTTestCase traktowany jest tak jakby był odpalany po stronie klienta. Tzn. stawiany jest serwer, do którego możemy się odwoływać przez GWT RPC. Działają też takie mechanizmy jak np. RootPanel.get().

Nobody has commented it yet.

Only logged in users can write comments

Developers World