RxJava dla JAX-WS. Poznajcie mój skrypt do automatycznego generowania wrapperów

Niedawno miałem niewątpliwą przyjemność pracować z SOAPowymi Web Services. Używałem narzędzia wsimport, aby wygenerować Javowe klasy proxy zgodne ze standardem JAX-WS. W teorii w nowoczesnych systemach takie Web Services już nie występują, ale w praktyce nieraz się z nimi spotykamy. Załóżmy dodatkowo, że nasza ładna i przejrzysta aplikacja używa RxJavy. Jest reaktywna i asynchroniczna, podoba nam się.


Patryk Lenza. Co-founder of Pattern Match / Solutions Architect and Engineer. Pragmatyczny pasjonat wszystkiego co wiąże się z wytwarzaniem oprogramowania. Uwielbia przeskakiwać pomiędzy technologiami i poznawać nowe rzeczy. Stara się wykorzystywać technologię do rozwiązywania biznesowych problemów. Zwolennik prostoty i przejrzystości, które prowadzą do elegancji.


Niestety interfejs wygenerowanych klas proxy bardzo odstaje od reszty naszego kodu, a my bardzo chcielibyśmy móc wywoływać te usługi traktując je jako typowe źródła Rx Observable/Completable. Jeśli wszystko to czego potrzebujemy to jednorazowe zaciągnięcie pliku WSDL i wygenerowanie z niego klasy proxy… to nie ma problemu. Po prostu utwórzmy sobie niewielkie klasy-wrappery, które otoczą proxy JAX-WS przez Observable/Completable i użyjmy je. Jednak, gdy usługi, które konsumujemy są aktywnie rozwijane i/lub musimy konsumować wiele różnych endpointów wystawiających osobne WSDLe — wtedy takie manualne tworzenie i aktualizowanie wrapperów stanie się zarówno męczące, jak i podatne na błędy.

I to właśnie mnie spotkało. Po początkowym zniechęceniu i małym poddenerwowaniu, postanowiłem sklecić mały i prosty skrypt do automatycznego generowania takich wrapperów. Mały komentarz zanim przedstawię pełne rozwiązanie: jest to szybki i brudny hack. Doskonale zdaję sobie sprawę, że idealnie i profesjonalnie można by to rozwiązać stosując procesory javowych adnotacji lub zaawansowane użycie silnika szablonów, lub wręcz tworząc osobne narzędzie podobne do wsimport. Potrzebowałem jednak czegoś na szybko. I jako bonus miałem okazję podłubać trochę w Gradle’u i jego potencjale do obsługi skryptów w Groovy’m.

Rozwiązanie

Całość kodu można zobaczyć na naszym repo w GitHub. Wszystko jest w zasadzie proste. Jedyne co musimy zrobić to wygenerować klasy proxy poprzez wsimport i następnie przeanalizować je używając paru regexów. Finalnie, mając już dane z analizy, wypełniamy krótkie szablony i zapisujemy wynik do Javowych plików. Zatem do dzieła.

Generowanie plików proxy JAX-WS

Niestety z powodów NDA nie mogę pokazać prawdziwych plików WSDL. Udało mi się jednak znaleźć dość skomplikowany ogólnodostępny przykład: http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx?wsdl — na początek wygenerujmy dla niego klasy proxy. Upewnijmy się, że w katalogu, w którym obecnie jesteśmy mamy  podkatalogi tmp i src/main/java — ich brak powoduje mało czytelny błąd wykonania wsimport. Odpalamy:

Katalog tmp nie interesuje nas w przeciwieństwie do źródeł, które zostaną wygenerowane do src/main/java/com/cdyne/ws. Ten post nie dotyczy jednak automatyzacji odpalania wsimport, dlatego kontynuujmy dalej, ale warto zaznaczyć, że Gradle ma odpowiednie pluginy, a również samodzielnie możemy podpiąć wywołanie do kodu skryptu, który tutaj omawiamy. Polecam przejrzeć wygenerowane pliki proxy. Jest ich dość sporo a ich kod jest typowy dla klas proxy — mocno techniczny i pełen powiązań z biblioteką JAX-WS.

Rozszerzanie pliku build.gradle o task, który generuje pliki RxJavy

Zmiany w pliku build.gradle są proste i krótkie:

Mamy tu task o nazwie genrx, który wykonuje nasz skrypt tools/rx.groovy przekazując mu 4 parametry. Te parametry możemy albo przekazać z linii poleceń, albo jeśli ich nie podamy zostaną pobrane domyślne wartości (drugi parametr 4 wywołań propertyOrDefault). Ja ustawiłem te domyślne wartości na dopasowane do przykładowego pliku WSDL oraz zadanego katalogu. W realnym scenariuszu wystarczy ustawić je na zgodne z plikiem, nad którym pracujemy, co wyeliminuje konieczność przekazywania ich do skryptu z linii poleceń.

Aby wykonać skrypt bez parametrów i z domyślnymi wartościami:

A jeśli chcemy przekazać parametry:

Przyda się krótki opis parametrów:

  1. serviceDir — katalog, do którego wsimport wygenerował klasy proxy. Może rozpoczynać się od / i wtedy jest ścieżką absolutną lub bez / i wtedy jest to podkatalog dla punktu wywołania Gradle.
  2. rxFileDir — katalog, do którego wygenerowane zostaną klasy RxJavy. Analogicznie z / jak dla parametru powyżej.
  3. rxFileName — wrappery wygenerowane zostaną do pliku o takiej nazwie. Należy również podać rozszerzenie na przykład Backend.java.
  4. rxFilePackageName — nazwa pakietu, w którym znajdą się wygenerowane wrappery Rx.

Skrypt rx.groovy — główne kroki z lotu ptaka

Generowanie pliku z wrapperami Rx Javy składa się z kilku głównych kroków:

Wyszukiwanie pliku zawierającego JAX-WS Web Service

To dość przyziemna robota. Normalizujemy ścieżkę do katalogu z plikami proxy (przekazaną w parametrze) i odnajdujemy plik, który zawiera klasę rozszerzającą Service z JAX-WS. W zasadzie nic interesującego.

Wyciąganie detali z pliku z JAX-WS Web Service

Mając znaleziony plik z proxy możemy załadować jego zawartość. Na wstępie musimy znaleźć do jakiego pakietu należy klasa, jaka jest jej nazwa, jaka jest nazwa Portu SOAP WS i jaka metoda jest wykorzystywana do otrzymania instancji tego Portu. Port jest tutaj najbardziej interesującym nas elementem, gdyż to właśnie on wystawia metody, które wywołują konkretne usługi po stronie backendu. A jest to dokładnie to co chcemy opakować Rxami. Cztery dość proste regexy z grupami pozwolą nam wyciągnąć potrzebne informacje. Polecam przejrzeć plik proxy z Web Service, wtedy regexy będą wydawać się jeszcze prostsze. Jedyna ciekawsza rzecz wymagająca wyjaśnienia dotyczy Portu. W klasie z usługą znajduje się metoda, która pozwala na otrzymanie jego instancji. To zawsze jedyna metoda, która rozpoczyna się od prefixu get i w jej ciele można znaleźć nazwę klasy Portu:

Stąd możemy dowiedzieć się w jakiej klasie zdefiniowany jest nasz Port — regex szuka ciągu znaków kończącego się na .class, aby otrzymać nazwę klasy oraz na metodę rozpoczynającą się od get, aby wiedzieć, co ma wywołać, aby otrzymać instancję.

Wyciąganie detali z klasy Portu

Na obecną chwilę znamy zatem nazwę klasy Portu i możemy załadować jej plik źródłowy. To najbardziej nas interesujący plik, gdyż tutaj są wywołania odpowiednich usług z backendu. Musimy go dobrze przemielić. Ja najpierw przygotowuję go sobie wycinając nieużyteczne komentarze, adnotacje czy puste linie:

Nadmienię, że nie jest to konieczne i reszta działa bez tego wycinania, ale po prostu lepiej analizuje i debuguje się potem taki uproszczony plik. Plik ładowany jest do jednego dużego Stringa, a następnie odpalanych jest parę regexów. (?s) nakazuje regexowi traktować znaki nowych linii jako wpadające w zakres dowolnego znaku, czyli . — wielolinijkowy String staje się dzięki temu jednym długim wejściem dla regexa. @\w+\((.*?)\) szuka @, po którym następują znaki alfanumeryczne lub _ i potencjalnie pusta lista parametrów: (, ). Operator .*? sprawia, że grupy przestają być zachłanne (greedy) zatem kończą dopasowanie przy pierwszym zamykającym ). Jest to niezwykle istotne, gdyż bez tego dopasowalibyśmy cały tekst aż do ostatniego ) w kodzie, a nas interesuje tylko ta jedna grupa parametrów adnotacji. (?m) zamienia regex w tryb wielolinii i pozwala używać ^ i $, aby znajdować w naszym długim stringu koniec i początek linii. Następnie standardowy wzorzec na wyszukanie końca linii: (?:\r?\n|\r))+ — wymaga przynajmniej jednego \n lub \r lub \r\n — i będzie działał na różnych formatowaniach kodu.

Wyciąganie publicznych metod web-service wraz z ich parametrami i typem zwracanym

To clue naszej ekstrakcji. Wystarczy jeden regex:

Szukamy tekstu, przed którym występuje nowa linia, następnie białe znaki i słowo public. Co dalej to łapiemy do grupy typ zwracany, nazwę metody i wszystko co jest wewnątrz ().

Opakowywanie znalezionych metod w Observable/Completable z RxJavy

Mamy więc dane wszystkich metod usługi. Nadszedł czas na wygenerowanie dużego stringa z kodem, który opakuje te wywołania w RxJavę. Możemy do tego użyć Groovy i jego silnika szablonów:

Potrzebujemy, aby dla każdej znalezionej publicznej metody usługi, która ma typ zwracany inny niż void, wygenerował się kod wyglądający podobnie jak ten konkretny przykład:

Jest to tylko i wyłącznie wywołanie oryginalnej metody na instancji Portu opakowanej w Observable. Za chwilę wyjaśni się skąd wzięła się ta instancja.

Jeśli oryginalna metoda zwraca void powinniśmy zwrócić Completable zamiast Observable. RxJava 2.0 nie pozwala na zwracanie null, dlatego nie możemy użyć Observable<Void>, co kiedyś było praktykowane w RxJava 1.0. Przykładowy kod dla voida wyglądać powinien jak:

Generowanie metod Rx to wykonywanie szablonu dla każdej znalezionej metody usługi. Detale tych znalezionych metod mamy w grupach złapanych z wcześniejszego regexa (w Matcher). Musimy tylko dla każdej metody przygotować zmienne, które staną się wejściem dla szablonu:

Dałoby się wprowadzić instrukcje if do środka szablonu i w odpowiednich przypadkach generować kod dla Observable lub Completable, ale podejście z prostym szablonem i dynamicznym wejściem wydało mi się czytelniejsze i łatwiejsze w przyszłej obsłudze.

Generowanie finalnego pliku z wywołaniem metod usługi w RxJavie

Na tym etapie mamy już wszystkie dane potrzebne do wygenerowania finalnego pliku źródłowego Javy. Wiemy do jakiego katalogu go generować, jaka ma być jego nazwa, jaki pakiet. Znamy nazwę klasy i pakiet oryginalnej usługi, portu i jak się dostać do instancji tego portu. No i oczywiście mamy kod metod Observable/Completable, które opakowują oryginalne wywołania. Do dzieła zatem:

Po raz kolejny wykorzystamy szablony Groovy. Szablon to treść pliku Javy. Przekazujemy do niego parametry, którymi są wszystkie detale, które zgromadziliśmy do tej pory. Wszystko wskakuje na swoje miejsce i formuje kompletny plik źródłowy.

Uwaga: wynikowy plik po cichu nadpisze istniejący!

Jak wygląda taki finalny plik?

Jest to normalny plik źródłowy Javy. Ma nawet konstruktor, który ukrywa tworzenie oryginalnej usługi i portu, dzięki czemu kod wywołujący nie będzie musiał troszczyć się o to i nie będzie tym zaśmiecony. Pozostawiam jako zadanie dodatkowe, aby nadbudować nad tą klasą interfejs lub pozwolić na wstrzykiwanie zależności przez drugi konstruktor, co sprawi, że wrapper będzie przyjaźniejszy w testowaniu.

Wywołanie wrappera w naszym ładny kodzie to już typowy kod używający RxJavy. Dla kompletności pełne wykorzystanie Observera:

I dostajemy wynik:

Finisz

Pominąłem detale niektórych prostszych lub mniej ciekawych pomocniczych funkcji. Można je zobaczyć w pełnym kodzie na naszym repo w GitHub

Koniec końców miałem dużo frajdy tworząc ten mały tool. Pokazuje on potencjał jaki daje Groovy wraz z jego bardzo dobrą integracją z Gradle. Parę regexów i szablonów naprawdę może dużo!


Artykuł pierwotnie został opublikowany pattern-match.com.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Startupowa prasówka: 16-20.07.2018