Play Framework – let’s go reactive!

W poprzednich wpisach pokazałem:

  • Jak stworzyć prostą aplikację REST za pomocą Play’a
  • Jak zwalidować dane przychodzące
  • Jak dodać obsługę bazy danych

Do tej pory pisałem w sposób klasyczny, stosowałem wiele uproszczeń.  Schemat działania aplikacji wyglądał mniej więcej tak:
Dane przychodzące -> walidacja -> kontroler -> baza danych -> wynik -> dane wychodzące.
Brak serwisów, DAO.
Robiłem to z premedytacją, aby pokazać pewne elementy bez komplikującej wszystko otoczki.

Teraz zróbmy to jak należy 🙂

Play działa w zgodzie z reactive manifesto

A to oznacza, że Play ma funkcje, dzięki którym nasz system będzie reactive. Co to właściwie oznacza ?

System, który jest reactive charakteryzuje się poniższymi cechami:

  • responsive – system odpowiada w jak najkrótszym czasie
  • resilient – system działa nawet, gdy któryś z komponentów ulegnie awarii
  • elastic – system jest responsywny bez względu na obciążenie
  • message driven – system działa za pomocą wiadomości przekazywanych asynchronicznie.

Po więcej szczegółów zachęcam do odwiedzenia strony reactive manifesto oraz zapoznania się z moim starym wpisem

Jak do tego podchodzi Play? Na kilka sposobów:

  • stateless – bezstanowość – Play w odróżnieniu od innych systemów nie posiada stanu aplikacji, dzięki czemu skalowanie poziome (na wiele serwerów) jest banalne
  • akka – Play działa mocno we współpracy z Akką – systemem aktorów
  • reactive streams – jak akka, to i reactive streams, mimo, że nie widać ich na pierwszy rzut oka, to są. Było je trochę widać we wpisie dotyczącym walidacji danych

To co, napiszemy naszą aplikację w stylu reactive?

Wymagania

  • Znajomość podstaw programowania funkcyjnego w Java
  • Podstawy Play! z moich poprzednich wpisów
  • 30 minut wolnego czasu
  • Projekt z poprzednich wpisów

 

Aktorzy

Nasza aplikacja będzie używała aktorów. Czym są aktorzy i jak działają próbowałem opisać dawno temu w tych wpisach (są stare, ale istota działania pozostaje ta sama).

Wiadomości

Aktorom przekazujemy wiadomości. Budujemy prostego CRUD’a, a więc potrzebujemy wiadomości, które przekażą żądanie utworzenia, pobrania, edycji oraz usunięcia elementów.

Utwórzmy pakiet actors, a w nim klasę PersonProtocol:

Ma ona w sobie enuma, pola zawierające identyfikator elementu z bazy danych i obiekt Json, który przyjdzie do nas od klienta.

Będziemy przekazywali taki obiekt do aktora i na jego podstawie będziemy wykonywali jakąś akcję.

Póki co nic trudnego 😉

PersonActor

To teraz napiszmy aktora..

Aby klasa stała się aktorem, musi rozszerzać albo UntypedActor, albo AbstractActor. AbstractActor jest bardziej funkcyjny, więc jego użyjemy.

AbstractActor wymaga implementacji metody createReceive.

Metoda createReceive ma zwrócić obiekt typu Receive.
Dla obiektu typu Receive mamy buildera, który nam go zbuduje, na podstawie funkcji, które mu przekażemy.
Będziemy otrzymywali obiekt typu PersonProtocol, a więc mu ją przekażemy:

receivebuilder ma metodę match, za pomocą której dopasowujemy wiadomość, do tego co mamy robić, a potem tworzymy obiekt metodą build().

Ważna uwaga – tutaj mamy obsługę wiadomości, a więc powinna ona być pełna (exhaustive). W tej chwili obsługujemy tylko typ PersonProtocol. Powinniśmy dodać obsługę wszystkich możliwych typów. Nie będziemy ich jednak podawać. Mamy do tego metodę matchAny, która weźmie każdy inny typ. Dodajmy matchAny:

Metoda unhandled przekaże wiadomość do systemu aktorów z nadzieją, że ktoś to obsłuży.

Teraz pozostaje nam już tylko logika funkcji… Implementacja może wyglądać na przykład tak:

Dopasowujemy akcję do tego co jest w akcji personProtocol. Pozostaje już zwykła logika.
Utwórzmy kilka metod, które dodadzą, usuną, skasują, pobiorą osobę…

Są to tak naprawdę metody podobne do tych, które mieliśmy w kontrolerze.

Dodajmy je teraz do funkcji w receivebuilder:

sender().tell(…) oznacza, że wysyłamy wynik wykonania metody do „sendera” czyli, tego kto nam przysłał wiadomość.

Inicjacja aktora

Aby użyć tego aktora, musimy go zainicjować i przygotować go do wstrzyknięcia tam, gdzie potrzebujemy. Aktor jest zawsze typu ActorRef, a więc zwykły inject nie zadziała.

Utwórzmy moduł – jest to komponent Play’a, dzięki któremu możemy wykonać dowolną akcję przy starcie aplikacji.

Tworzymy pakiet modules i dodajemy klasę AkkaModule:

Klasa AkkaModule musi rozszerzać AbstractModule i implementować AkkaGuiceSupport, dzięki czemu możemy dodać klasę aktora do system Dependency Injection. Robimy to w metodzie configure, używając metody bindActor, która jako parametr przyjmuje klasę aktora oraz jego nazwę – taką, jaką sobie wymyślimy.

Włączamy jeszcze moduł w application.conf:

Kontroler

To czas na ostatni etap układanki, czyli kontroler:

Weźmiemy na warsztat PersonController z poprzedniego wpisu.

Po pierwsze, musimy wstrzyknąć aktora:

Czyli utworzyliśmy konstruktor, który jako parametr przyjmuje nazwanego personActora (@Named(„person-actor”), parametr musi mieć tą samą nazwę, jaką daliśmy w metodzie bindActor w module.
Można oczywiście sobie gdzieś te nazwy zdefiniować, tutaj użyłem prostych stringów.

Wstrzyknęliśmy też konwerter osoby na json.

Wiadomość do aktora przekazać możemy na dwa sposoby (są też inne, ale te są najczęściej stosowane):

  • metoda tell(..) – fire and forget – wysyłamy wiadomość i nie oczekujemy odpowiedzi
  • metoda ask(…) – wysyłamy wiadomość i oczekujemy na odpowiedź.

Od razu widać, że tell nas nie interesuje, bo wtedy nigdy nie otrzymamy odpowiedzi od aktora. Musimy go zapytać używając metody ask(…).

Co zwraca ask ? CompletionStage<> .

Czym jest CompletionStage ?

Niczym innym jak asynchroniczną odpowiedzią. Odpowiedzią, która nadejdzie w przyszłości. Tutaj jest największe zamieszanie. Prosimy aktora, który kiedyś zwróci nam odpowiedź. A jeśli kiedyś, to znaczy, że oczekując na odpowiedź nie blokujemy wątku.

Gdy mieliśmy logikę w kontrolerze, to na czas wykonania tego kodu, blokowaliśmy wątek. Co gorsza, mieliśmy tam operacje na bazie danych, które z natury są blokujące.
Używając aktorów, na czas wykonania zadania przez aktora, nie blokujemy wątku kontrolera, dzięki czemu może się on zająć innymi żądaniami.

Brzmi cudownie, ale jak to napisać?

Weźmy na warsztat metodę do pobierania pojedynczego wpisu:

Przeanalizujmy to:

Zwracamy CompletionStage od Result.

Wysyłamy wiadomość do aktora poprzez statyczną metodę ask z PatternsCS.
Jako parametry metody ask przekazujemy obiekt aktora, wiadomość (czyli nasz PersonProtocol) oraz czas w milisekunach, w którym będziemy oczekiwali na odpowiedź od aktora. Jeżeli po tym czasie nie przyjdzie odpowiedź, poleci nam brzydki exception – AskTimeoutException.

W kolejnych metodach thenApply mamy już obsługę tego, co przyjdzie od aktora. Niestety – nie ma czegoś takiego jak typowanie odpowiedzi, stąd też musimy odpowiedź od aktora rzutować na odpowiedni typ. Naturalnie, możemy go też najpierw sprawdzić.
W naszym przypadku na wiadomość GET_ONE powinniśmy otrzymać Optional<Person>, na to też rzutuję odpowiedź. Reszta to już prosta logika. Jeżeli Optional<Person> zwrócił Person, to zamieniam go na json’a i zwracam do klienta, a jeśli nie, to zwracam notFound

Kolejne metody działają dokładnie w ten sam sposób:

Kod zrobił się trochę bardziej skomplikowany, ale pomyślmy, co osiągnęliśmy – asynchroniczną aplikację, która nie blokuje wątku kontrolera, która działa na aktorach, którzy z kolei mogą być na zupełnie innej maszynie i może być ich wielu itp.

Uważny czytelnik może jednak stwierdzić – fajnie, nie blokujemy wątku kontrolera, ale blokujemy wątek aktora poprzez operacje na bazie danych.
Prawda. Blokujemy.
O tym, jak nie zablokować aktora operacjami na bazie danych, i czy to się w ogóle da zrobić, opowiem w kolejnym wpisie.