Jak działa RequestFactory w GWT?

Jakiś czas temu pisałem na temat alternatywnego mechanizmu komunikacji klient-serwer w GWT, jakim jest RequestFactory. Omówiliśmy wtedy podstawy – zaimplementowaliśmy generyczną klasę DAO i wykonywaliśmy proste operacje CRUD. Przy okazji artykułu na temat JPA i walidacji encji pokazałem, jak w prosty sposób przeprowadzić walidację encji i zintegrować to z RequestFactory. Podczas omawiania relacji wiele-do-wielu i zapytań poprzez Criteria API w JPA 2.0 rozbudowaliśmy projekt wykorzystujący RequestFactory, jednak wtedy nie skupialiśmy się na części klienckiej, gdzie wykorzystywany jest tytułowy mechanizm. W żadnym z tych artykułów nie pisałem jednak na temat zasad działania RequestFactory, a domyślam się, że jesteś ciekaw jak to wszystko chodzi. W niniejszym wpisie chciałbym się skupić właśnie na stronie technicznej.

RequestFactory jest mechanizmem wprowadzonym w GWT 2.1 w celu dostarczenia alternatywy dla GWT-RPC. Alternatywy, dlatego że GWT-RPC dalej pozostaje podstawowym i najprostszym sposobem komunikacji klient-serwer, które nie jest jednak pozbawione kilku wad. RequestFactory jest przede wszystkim zorientowane na pobieranie danych (data-oriented) i doskonale integruje się z framework’ami bazodanowymi takimi jak np. Hibernate. Implementuje też walidację encji według normy JSR 303. Tak naprawdę mechanizm nadaje się do użytku od GWT 2.1.1, w której wprowadzono szereg udoskonaleń. W następnych wersjach pojawiały się mniejsze poprawki, zostały też zmienione pakiety wszystkich klas na mniej związane z samym GWT (z com.google.gwt na com.google.web.bindery), co sugeruje, że być może kiedyś zostanie wykorzystane poza GWT. W tym artykule opieram się na RequestFactory, jakie jest w GWT 2.4.

UWAGA: Poniższy tekst nie jest jeszcze kompletny. Zdecydowałem się na jego wcześniejsze opublikowanie, gdyż i tak już od kilku miesięcy zalega mi w zapleczu, a ja nie mogę znaleźć motywacji na jego ukończenie :) Wpis będzie jeszcze uaktualniany, więc zachęcam do jak największej ilości komentarzy w celu wyjaśnienia niezrozumiałych zagadnień. W przyszłości pojawią się też przykładowe fragmenty kodu.

Podstawy

RequestFactory bardzo dobrze oddziela część serwerową kodu od części klienckiej, dzięki czemu programista nie musi samodzielnie implementować klas przenoszących dane (DTO) oraz przepakowywać dane z obiektów encji do DTO. Encje w RequestFactory są normalnymi klasami POJO, w których możemy używać adnotacji Hibernate, JPA i innych. Oprócz encji, po stronie serwera występują również serwisy operujące na obiektach encji, które mogą występować w postaci osobnych klas DAO bądź w postaci metod w klasach encji. Co ważne, mamy tu pełną dowolność i możemy stosować w projekcie obydwa sposoby.

Strona kliencka, to przede wszystkim interfejsy. RequestFactory silnie korzysta z możliwości automatycznego generowania kodu i implementuje za nas wszystkie niezbędne do zapewnienia komunikacji klasy. Rola programisty sprowadza się do określenia interfejsów Proxy dla obiektów domenowych oraz serwisów operujących na tych obiektach. Interfejsy Proxy odzwierciedlają klasy encji, a w zależności od ilości wyspecyfikowanych getter’ów i setter’ów ograniczają dostęp do właściwości obiektów encji.

Zależność 1:1 między stroną serwerową a kliencką nie jest konieczna. Można zaimplementować np. klika interfejsów dla jednej encji, bądź kilka interfejsów serwisów dla jednej klasy DAO.

  • część serwerowa: klasy encji i implementacja serwisów
  • część kliencka: interfejsy Proxy i interfejsy serwisów
  • możliwość zdefiniowania wielu interfejsów dla odpowiedników serwerowych

Obiekty domenowe

Obiektami domenowymi w RequestFactory są obiekty encji i obiekty wartości. Odróżniane są za pomocą interfejsu, który rozszerzany jest przez interfejs proxy dla danej klasy po stronie serwera – odpowiednio EntityProxy i ValueProxy. Obydwa interfejsy mogą posiadać metody getterów i setterów dla właściwości obiektów, jednak sama ich obsługa mocno się różni.

Obiekty wartości są po prostu prostymi obiektami, służącymi do przesyłania kilku właściwości obiektu na raz. Każde wywołanie zdarzenia aktualizacji takiego obiektu, skutkuje przesłaniem całej jego struktury. Oprócz tego RequestFactory nie wymaga implementowania żadnych dodatkowych metod dla obiektów wartości, więc mogą być one z powodzeniem wykorzystywane w zastępstwie mechanizmu GWT-RPC.

Tak naprawdę główną siłą RequestFactory są jednak obiekty encji. Wszystkie encje muszą posiadać identyfikator, a oprócz tego GWT wymaga, aby posiadały też atrybut wersji (w JPA dobrze jest do niego użyć adnotacji @Version). Interfejsy encji mogą zawierać w sobie odwołania do interfejsów obiektów wartości. Po stronie klienta cały mechanizm monitoruje wszystkie zmiany przeprowadzane na obiektach encji, a do strony serwerowej przesyła tylko zmiany. Atrybut wersji używany jest właśnie w celu rozpoznania, czy po stronie serwera nie zaszły wcześniej inne zmiany i modyfikowana encja jest we właściwym stanie. Wszystkie akcje przeprowadzane na encjach bezpośrednio po stronie serwera powodują wysłanie obiektów zdarzeń po stronie klienckiej. Operacje na obiektach encji znakomicie ograniczają więc ilość danych przesyłanych przez sieć, co może być szczególnie znacznym atutem przy projektowaniu aplikacji o ograniczonej przepustowości.

Interfejsy Proxy muszą za pomocą adnotacji @ProxyFor lub @ProxyForName wskazywać na właściwą klasę encji po stronie serwera. Dodatkowo w adnotacji można określić klasę lokatora encji (implementującą interfejs EntityLocator). Lokator encji lub sama encja (jeśli lokator nie został określony) musi implementować metody pobierające identyfikator i wersję encji oraz pozwalające na wyszukanie encji o danym identyfikatorze lub utworzenie nowej jej instancji. Jeśli użyjemy adnotacji @ProxyFor, mechanizm RequestFactory (a dokładniej klasa RequestFactoryGenerator) sprawdzi poprawność interfejsów Proxy.

Podsumowując, mamy w skrócie tak:

  • obiekty wartości służą do przesyłania grup atrybutów
  • obiekty encji, jako warstwy dla operacji na właściwych encjach serwerowych
  • obiekty encji posiadają identyfikator i wersję
  • obiekty encji redukują ilość przesyłanych danych

Serwisy

Serwisy składają się z metod, które mogą przyjmować i/lub zwracać obiekty domenowe (obiekty encji i obiekty wartości) bądź typy proste. Większość z tych metod zazwyczaj implementuje akcje CRUD (tworzenie, odczyt, aktualizacja, usuwanie) na encjach, ale używając typów prostych i obiektów wartości można implementować metody realizujące podobną funkcjonalność co GWT-RPC.

Metody mogą operować jedynie na swoich atrybutach bądź dodatkowo na aktualnych instancjach encji. Te pierwsze implementowane są jako metody statyczne w encjach lub metody instancji w klasach serwisów i zwracają obiekty Request<T>, gdzie T jest właściwym typem zwracanym przez metodę serwisu po stronie serwera. Drugie to metody instancji encji, a zwracany typ to InstanceRequest<P, T>, gdzie P to interfejs Proxy danej encji, a T to typ zwracany w serwisie po stronie serwera.

Interfejsy serwisów po stronie klienta muszą rozszerzać interfejs RequestContext i za pomocą adnotacji @Service lub @ServiceName określać właściwą implementację serwerową. Implementacja serwerowa może znajdywać się w klasie encji lub osobnej klasie – wtedy konieczne jest również podanie lokatora serwisu (implementującego interfejs ServiceLocator). Lokator serwisów odpowiedzialny jest za powoływanie nowych instancji serwisów. Dodatkowo jeśli użyjemy adnotacji @Service, RequestFactory sprawdzi poprawność wszystkich metod serwisów.

Wszystkie serwisy po stronie klienta generowane są automatycznie na podstawie zadeklarowanych interfejsów. Odpowiada za to obiekt implementujący interfejs RequestFactory, który powoływany jest za pomocą metody GWT.create(). Wszystkie metody tego obiektu pełnią rolę fabryk, które przy każdym wywołaniu powołują nowe instancje pieńków serwisów.

Ponownie podsumowując:

  • po stronie klienta interfejsy serwisów z definicjami metod
  • po stronie serwera metody statyczne encji lub metody instancji serwisów operujące tylko na atrybutach metod
  • po stronie serwera metody instancji encji operujące na atrybutach metod i aktualnej instancji

Jak to wszystko ugryźć?

Aby cały mechanizm poprawnie działał, w aplikacji musi zostać zdefiniowany serwlet RequestFactoryServlet, który powinien być dostępny w lokalizacji /gwtRequest. Po stronie klienta pierwszą czynnością jest powołanie obiektu implementującego interfejs RequestFactory za pomocą GWT.create(). Aby przygotować obiekt do użycia, należy uruchomić na nim metodę initialize(), przyjmującą jako atrybut szynę zdarzeń (obiekt EventBus). Szyna zdarzeń używana jest do do przesyłania zdarzeń o zmianach zachodzących w encjach. Metoda initialize() posiada również opcjonalny parametr przyjmujący obiekt interfejsu RequestTransport. Obiekt ten określa sposób wywoływania strony serwerowej i domyślnie wysyła komunikaty właśnie do lokalizacji /gwtRequest. Możemy tu przekazać własną implementację tego interfejsu lub obiekt klasy DefaultRequestTransport, w którym ustawimy inną ścieżkę do wywołań serwletu.

Nasz obiekt interfejsu rozszerzającego interfejs RequestFactory powinien posiadać metody zwracające obiekty implementujące zdefiniowane wcześniej interfejsy rozszerzające RequestContext. Wywołując te metody dostajemy obiekty czegoś w rodzaju kontekstu wywołania serwisu. Dopiero w ramach tego kontekstu jesteśmy w stanie modyfikować lub tworzyć nowe instancje obiektów domenowych (czyli nasze interfejsy rozszerzające EntityProxy i ValueProxy). Przykładowo, jeśli chcemy zmodyfikować obiekt domenowy, należy najpierw wywołać metodę edit(). Metoda ta jako argument przyjmuje nasz obiekt, a zwraca nową, modyfikowalną jego wersję – dopiero teraz można taki obiekt modyfikować i wysłać ponownie na serwer. Podobnie aby utworzyć nowy obiekt domenowy, należy użyć metody create(), która również zwraca nam modyfikowalną wersję. Należy pamiętać, że wszystkie obiekty domenowe, których modyfikowalne wersje nie zostały utworzone za pomocą powyższych metod, nie mogą podlegać zmianom – każde wywołanie na nich settera spowoduje wystąpienie błędu.

Wywołując metody na pobranym wcześniej kontekście serwisu pobieramy obiekt Request. Dopiero w momencie wywołania na nim metody fire() wysyłane jest żądanie http do serwletu RequestFactory. Metoda ta może dodatkowo przyjmować obiekt klasy Receiver, który pełni rolę odpowiednika AsyncCallback’a z GWT-RPC.

Podsumowanie

Jak RequestFactory wypada przy podstawowym mechanizmie GWT-RPC? W czym jest lepszy, a gdzie lepiej stosować „stary” sposób komunikacji? Na te pytania musisz częściowo odpowiedzieć sam, gdyż dopiero po kilku użyciach RequestFactory będziesz w stanie docenić zalety, jakie ze sobą niesie. Niestety można też napotkać kilka istotnych wad. Do jednej z nich należy ograniczony zakres typów danych obiektów przekazywanych przez serwisy RequestFactory – nie jest możliwe np. przesyłanie jakichkolwiek implementacji interfjsu Map.

Mimo wszystko w serwisach zorientowanych na dane RequestFactory sprawdza się lepiej. Najważniejsza jest tu możliwość rozdzielenia implementacji serwerowych i klienckich dla obiektów domenowych oraz w pełni automatyczna ich konwersja. W GWT-RPC albo sami musieliśmy zadbać o przepakowywanie encji do klas DTO, albo używać zewnętrznych rozwiązań typu Gilead (dawne hibernate4gwt), albo współdzielić te same implementacje po stronie klienckiej i serwerowej. Od momentu wprowadzenia RequestFactory nie musimy już się zastanawiać, jak przepakowywać dane, bo GWT robi to za nas.

Zachęcam też do przeczytania artykułów o RequestFactory autorstwa Thomasa Broyer’a – osoby zaangażowanej w rozwój GWT: Część 1, Część 2.

Skomentuj

4 Komentarze.

  1. Tekst bardzo fajny i praktyczny. To co mnie zaniepokoiło to „Poniższy tekst nie jest jeszcze kompletny. Zdecydowałem się na jego wcześniejsze opublikowanie, gdyż i tak już od kilku miesięcy zalega mi w zapleczu, a ja nie mogę znaleźć motywacji na jego ukończenie”

    Czekam na jego ukończenie :-)
    Zaginiony ostatnio napisał(a)… Zaginiony-znaleziony.pl – zaginione psy, koty

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge