News Radar: Play Framework, Slick, Dependency Injection i podział na moduły

Od ostatniego mojego wpisu na temat projektu minęła już chwila. Przez ten czas nie działo się wiele jeśli chodzi o nowe funkcje aplikacji, natomiast trochę sie pozmieniało pod względem różnych technikaliów. Projekt został podzielony na moduły, pojawiły się nowe biblioteki: Play Framework i Slick. Wprowadziłem mechanizm Dependency Injection. W tym artykule przybliże trochę te zmiany i wskażę na rzeczy, które popsuły mi trochę krwi – może komuś oszczędzi to w przyszłości trochę czasu.

Narracja w tym artykule może się wydać nieco chaotyczna, bo często „dotknięcie” jednego tematu powodowało problemy z innymi, ale mam nadzieję, że koniec końców artykuł będzie zrozumiały.

Play Framework

Play Framework jest jednym z najbardziej znanych frameworków do tworzenia aplikacji webowych, których można użyć w Scali. Kiedyś, kiedyś miałem krótkie podejście do Play’a ale nie udało mi się pobawić tym frameworkiem dłużej. Teraz to się zmieni :) Jedną z bardzo ciekawych rzeczy, którą Play potrafi jest szybkie deployowanie zmian. Kiedy serwer z odpalaną aplikacją działa, to w tym czasie nadal możemy robić zmiany w naszej aplikacji (czy to klasach Scalowych/Javowych, czy w konfiguracji, czy w HTMLu). Play będzie w stanie się zorientować co się konkretnie zmieniło. W momencie kiedy przeładujemy stronę, która w jakichś sposób korzysta ze zmienionych zasobów, to Play w tle je przekompiluje (jeśli trzeba) i dokona ich deploymentu przed właściwym przeładowaniem.

Konfiguracja SBT

Domyślnie zalecanym sposobem rozpoczęcia pracy z Play Framework jest użycie Activatora, który wygeneruje nam szkielet projektu. Z pomocą activatora możemy też projekt budować i uruchamiać. Ja miałem w planach używać tylko SBT, myślałem więc, że stworzenie projektu bez Activatora będzie ciężkie, ale wcale tak nie było. W zasadzie wszystko sprowadza się do tego, żeby w używać pluginu do SBT przygotowanego przez twórców Play Framework.

Pierwsze co trzeba zrobić to dodać plugin w pliku plugins.sbt:

lazy val playResolver = "Typesafe repository" at "https://repo.typesafe.com/typesafe/maven-releases/"
lazy val playPlugin = "com.typesafe.play" % "sbt-plugin" % "2.5.12"

resolvers += playResolver
addSbtPlugin(playPlugin)

Następnie należy użyć tego pluginu w definicji projektu w sbt:

enablePlugins(PlayScala)

I już możemy tworzyć aplikację z użyciem Play’a.

JavaScript i Ajax

Chwilę zajęło mi skonfigurowanie routingu tak aby można było wykonywać ajax calle z poziomu JavaScriptu w widokach w Play. Oto kolejne kroki:

  1. Jeden z naszych kontrolerów musi stworzyć definicje Java Script routingu
  2. W pliku z routingami Play’a musimy powiązać wcześniej zadeklarowaną metodę kontrolera z szablonem requestu
  3. Musimy wiedzieć w jaki sposób dodać zewnętrzne biblioteki javascript jeśli chcemy ich używać (ja np. Ajax calle robię za pomocą jQuery na ten moment)
  4. Musimy wiedzieć w jaki sposób odwołać się do zewnętrznych bibliotek z poziomu widoku
  5. Musimy umieć poprawnie się odwołać do kontrolera z poziomu kodu JS

Żeby zdefiniować nasz routing w kontrolerze, dodajemy sobie taką metodkę (u mnie w klasie FeedListController):

def feedJavascriptControllers = Action {
  implicit request => Ok(
    JavaScriptReverseRouter("jsRoutes")(
      /* Ponizej metody tego samego kontrolera, ktore beda wolane z poziomu JavaScript */
      controllers.routes.javascript.FeedListController.add,
      controllers.routes.javascript.FeedListController.load
    )
  ).as("text/javascript")
}

Ja umieściłem tą metodę w klasie FeedListController, która zawiera metody związane z obsługą Feed’ów, ale widzę, że popularną techniką, jest trzymanie takich konfiguracji w osobnym dedykowanym kontrolerze. Możliwe, że później dokonam takiej zmiany.

W pliku z routingiem dodajemy sobie taki wpis.

GET     /javascriptRoutes       controllers.FeedListController.feedJavascriptControllers

W tym momencie jesteśmy gotowi na robienie requestów z poziomu JavaScript.

Jak już wspominałem, dodałem do projektu jQuery za pomocą którego będę robić ajax requesty do mojego kontrolera. W jaki sposób dodać dodatkowe biblioteki JS? Po pierwsze musimy je „fizycznie” umieścić w projekcie jako assety. Żeby to zrobić – ściągnięte i rozpakowane jQuery umieszczamy w katalogu /public/jquery. Teraz, żeby móc do jQuery odwołać się w widoku, musimy dodać odpowiedni wpis do routingów:

GET     /assets/*file           controllers.Assets.at(path="/public", file)

Żeby zaimportować skrypt do widoku używamy takiej konstrukcji:

<script type="text/javascript" src='@routes.Assets.at("jquery/jquery-3.1.1.min.js")'></script>

Dzięki odpowiednio zdefiniowanemu routingowi plik zostanie odnaleziony. Odrazu wspomnę, że taka konfiguracja routingu działa, jężeli gui pisane w Play Framework jest root modułem naszego projektu (albo jeżeli cały nasz projekt mieści się w jednym module). Nieco dalej napiszę, co zrobić, żeby routing działał w momencie, kiedy moduł gui nie jest rootem.

Ostatnią sprawą jest to, jak wykonać calla z poziomu JavaScript. Dla powyższej konfiguracji routingu wyglądać to będzie w ten sposób:

$.ajax({
  url: "@routes.FeedListController.load",
  type: 'GET'
});

Podział projektu na moduły

Podzieliłem projekt na kilka modułów – narazie bardzo wstępnie i na pewno ten podział jeszcze będzie się zmieniać. Podział na moduły daje kilka profitów:

  • Łatwiej przeglądać kod – przeglądając moduł „gui” wiemy, że znajdują się tam klasy związane z interfejsem użytkownika, a nie np. z komunikacją z bazą danych
  • Łatwiej zapanować nad zależnościami wewnątrz projektu – ponieważ możemy zadeklarować jak będzie wyglądać sieć zależności między modułami.
  • Możemy wydzielić moduły, które mają specjalne wymagania. Przykładowo Play Framework domyślnie wprowadza konkretny podział na katalogi. Jednym z wyjść jest przekonfigurowanie Play’a tak żeby używał innej struktury katalogów. Ale możemy też poprostu część gui zapakować do osobnego modułu. Moduł ten będzie miał typowo Playowy podział na katalogi, natomiast inne moduły mogą mieć np. podział typowy dla SBT.
  • Przemyślany podział na moduły typu interfejsy i moduły typu implementacje pozwoli nam łatwo podmiany w razie potrzeby. Przykładowo – dzisiaj możemy trzymać jakieś dane w bazie danych, ale w przyszłości może się okazać, że np. będziemy chcieli korzystać z danych dostarczanych przez jakiegoś zewnętrznego providera. Samo rozdzielenie na interfejs i implementacje to oczywiście podstawa, ale jeżeli dodatkowo implementacje dostępu do tych danych będą w innych modułach, to podmianę możemy zróbić z poziomy SBT.

Podział projektu w SBT

Teoretycznie podział na moduły w SBT jest bardzo prosty. Przede wszystkim musimy stworzyć sobie w SBT obiekty typu projekt (będę pomijać wszystkie inne settingsy z build.sbt, żeby nie zaciemniać przykładów):

lazy val backend = project in file("backend") // taka notacja oznacza, że moduł znajduje się w katalogu "backend"
lazy val infrstructure = project in file("infrastructure")
lazy val gui = project in file("gui")
lazy val root = project in file(".")

Następnie musimy zadeklarować zależności między projektami:

lazy val backend = project in file("backend")

lazy val infrstructure = project in file("infrastructure")
  .aggregate(backend)
  .dependsOn(backend)

lazy val gui = project in file("gui")
  .aggregate(backend)
  .dependsOn(backend)

lazy val root = project in file(".")
  .aggregate(gui, backend, infrastructure)
  .dependsOn(gui, backend, infrastructure)

Mamy tutaj dwa rodzaje deklaracji zależności. Metoda aggregate definiuje nam zależność z punktu widzenia buildu SBT – jeżeli projekt A ma zależność aggregate do projektu B, to w procesie budowania, projekt B zostanie zbudowany pierwszy (nawet jeżeli wprost budujemy tylko projekt A). Metoda dependsOn z kolei pozwala nam na poprawne skonfigurowanie classpath’a: jeżeli projekt A ma zależność dependsOn do projektu B, to klasy z projektu B są widoczne dla projektu A.

Trochę offtopicowo – komuś mogło się rzucić w oczy, że moduł infrastructure zależy od projektu backend a nie na odwrót jakby podpowiadała logika. Dzieje się tak dlatego, że opieram się trochę na podejściu heksagonalnym – gdzie centrum aplikacji jest domena (w tym wypadku projekt o narazie może niefortunnej nazwie „backend”) – i to w niej istnieje interfejs typu DAO, jednak jego implementacja jest już w infastrukturze. Inaczej mówiąc to domena „żąda”, żeby istniała implementacja takiego interfejsu a infrastruktura to żądanie spełnia.

I teraz jeszcze jedna ważna sprawa. Generalnie kiedy robimy modułowy projekt w sbt, to każdy z modułów może mieć w sobie swoje build sbt. Mój pierwszy pomysł był taki, żeby build.sbt znajdujący się w głównym katalogu zawierał tylko definicję zależności między projektami a pliki build.sbt w poszczególnych modułach defniowały wszystko inne. Niestety to ot tak nie zadziała. Moja wiedza o sbt jest bardzo mała, więc strzelam, że po prostu konfiguracja z root build.sbt zostaje nadpisana. Napewno jeszcze kiedyś wrócę do tego tematu, ale póki co trochę mi to krwi napsuło, więc narazie zostanie tak, że root build sbt będzie zawerać całą konfigurację.

Druga ważna sprawa – wcześniej pisałem, że większość konfiguracji sbt dla PlayFramework to dodanie pluginu PlayScala – w przypadku aplikacji o takiej konfiguracji jak moja – ten plugin powinien być też podpięty do roota – dzięki temu możemy wywołać sbt run na projekcie root.

Zmiany w konfiguracji modułu opartego na Play Framework

Na ten moment w części GUI mam dodanego bootstrapa i jquery jako statyczne pliki serwowane przez Playa (zastanawiam się nad tym czy aby nie przejść na webjars, ale to temat może na kiedy indziej). W momencie kiedy aplikacja pisana w Playu jest twoim root modułem to dostęp do takich plików jest bardzo prosty do skonfigurowania wg. tego co mówi nam dokumentacja (co zresztą pokazałem wcześniej). Często też możemy spotkać się z sytuacją, gdzie pod głównym modułem napisanym z użyciem Playa mamy kilka innych pod-modułów również Playowych. Informacje o tym jak serwować statyczne pliki dla takiej konfiguracji również jest względnie prosta do znalezienia. Natomiast konfiguracja, którą mam ja – czyli root moduł nie będący Playową aplikacją – to nie jest najczęściej spotykana konfiguracja. Dlatego też poniżej przedstawiam w jaki sposób skonfigurować serwowanie statycznych plików.

Przypominam, standardowo ustawienie routingu dla assetów wygląda mniej-więcej tak:

GET   /assets/*file   controllers.Assets.at(path="/public", file)

Co jednak się stanie jeżeli nasz moduł, nie jest rootem całego projektu? Ja bym obstawiał (dla modułu w katalogu „gui”), że link do assetów będzie miał postać podobną do /gui/public. Tak jednak nie jest. Żeby znaleźć nasze assety, musimy wiedzieć dwie rzeczy. Po pierwsze to, że katalogiem z którego zaczynamy szukać będzie /public/lib. Po drugie, musimy znać nazwę modułu a nie tylko katalog w którym się znajduje – nazwa ta jest zdefiniowana w build.sbt – w moim wypadku będzie to „news-radar-gui”. Z tych dwóch rzeczy wynika, że poprawnym routingiem będzie:

GET   /assets/*file   controllers.Assets.at(path="/public/lib/news-radar-gui", file)

Jeżeli jednak z jakiegoś powodu nie możemy sami „dojść do ładu” jak skonfigurować routing, to alternatywną techniką jest ręczne przeszukanie katalogu target dla głównego modułu aplikacji. W moim przypadku assety znajdują się w katalogu: /target/web/public/main/lib/news-radar-gui – nie jest to może 1:1 katalog który wpisujemy w routing ale na podstawie tego i informacji z dokumentacji Play’a byłem wstanie już dość szybko dojść do poprawnej ścieżki.

Slick

Postanowiłem użyć biblioteki Slick – obok Play Framework’a jest to kolejny element Stacku Lightbendowego w mojej aplikacji (zastanawiam się jeszcze, czy nie użyć jeszcze Akki, ale nie uprzedzajmy faktów). Slick jest biblioteką ułatwiającą nam korzystanie z baz danych. Twórcy Slicka mówią o nim, że to Functional Relational Mapper w przeciwieństwie do klasycznych rozwiązań ORM. Idea Slicka i sposób korzystania z niego wydaje mi się bardzo ciekawy – może kiedyś napiszę jeszczę ogólniejszego posta na ten temat. Dzisiaj tylko w spomnę o jednym małym triku, który narazie dla mnie jest przydatny. Na ten moment używam bazy h2, która to jest bazą danych in memory. Potrzebowałem jakiegoś sposobu na to, żeby schema tabeli tworzyła się sama ale tylko jeden raz. Z pomocą przyszedł mi nieoceniony stackoverflow ;) (niestety uciekł mi link do tamtego tematu):

Await.result(
  db.run(MTable.getTables).flatMap(
    v => {
      val names = v.map(m => m.name.name)
      val create = Seq(table).filter(
        t => !names.contains(t.baseTableRow.tableName)
      ).map(_.schema.create)
      db.run(DBIO.sequence(create))
    }
  ), Duration.Inf)

Umieszczamy ten fragment w ciele naszego DAO, tak więc zostanie on wywołany w momencie stworzenia jego instancji. Schemat działania jest bardzo prosty – pobieramy listę wszystkich stworzonych tabel i sprawdzamy czy na tej liście znajduje się tabela, którą chcemy stworzyć. Jeśli nie to tworzymy zapytanie i odpalamy je. To wszystko razem jest opakowane Awaitem, gdyż operacje na bazie w Slicku co do zasady są nieblokujące i zwracają Future[].

Dependency Injection z Guice

Żeby rozdzielanie na moduły typu interfejs i implementacja miały sens, warto mieć mechanizm dependency injection, który będzie wszystko składał do kupy. Play Framework domyślnie przychodzi ze wsparciem dla Google Guice, które tutaj wykorzystamy. Żeby to wszystko zadziałało musimy stworzyć troszeczkę konfiguracji. Po pierwsze, żeby wstrzykiwanie działało dla naszych kontrolerów, dodajemy konfigurację do projektu gui w build.sbt:

routesGenerator := InjectedRoutesGenerator

Po drugie musimy wskazać położenie modułu Guice’owego (o ile nie zamierzamy go trzymać w domyślnej lokalizacji) – robi się to w pliku application.conf dla modułu Play’owego:

play.modules.enabled += "utils.InjectModule"

Po trzecie musimy mieć sam moduł Guice’a – u mnie jest to klasa na poziomie modułu rootowego w katalogu w zgodzie z konwencją Play Frameworka: /app/utils/InjectModule.scala (nie próbowałem umieszczać go w innym katalogu więc nie wiem jak to zadziała) – na dzień dzisiejszy wygląda on tak:

class InjectModule extends AbstractModule {
  def configure() = {
    bind(classOf[FeedService]).to(classOf[FeedServiceImpl])
    bind(classOf[FeedDao]).to(classOf[FeedDbDao])
    bind(classOf[Database]).toProvider(Db2DatabaseProvider)
  }
}

Widać tutaj przykład bindowania zarówno po przez wprost podanie pary trait/implementacja jak i przez podanie pary triat/provider.

Ale żeby w ogóle wstrzykiwanie mogło zainstnieć, musimy jeszcze dodać odpowiednie anotacje z JSR-330. Przykładowo w jednym z serwisów:

class FeedServiceImpl @Inject() (dao:FeedDao) extends FeedService {
  override def storeFeed(name: String, address: String): Unit = dao.store(new Feed(0, name, address))
  override def loadFeeds: Seq[Feed] = dao.load
}

W powyższym przypadku obiekt klasy FeedDao jest wstrzykiwany do serwisu.

Jest jednak jeszcze jedna rzecz o której musimy pamiętać. Tylko moduł gui w moim projekcie ma zależności do Guice, a dzięki czemu i do JSR-330 – w pozostałych projektach musimy dodać JSR-330 ręcznie jako zależność w build.sbt

lazy val javaxInject = "javax.inject" % "javax.inject" % "1"

Podsumowanie

Jak widać niewiele się działo w projekcie pod względem nowych funkcjonalności, natomiast trochę się pozmieniało „pod maską”. Mam jeszcze w planach zrobienie paru rzeczy technicznych – np. nie chce mi się za bardzo dłubać czystym jQuery, więc prawodpodobnie jednym z następnych kroków będzie podpięcie Angulara, bądź czegoś podobnego. Prawdopodobnie bardziej przemyślę jeszcze podział na moduły i postaram się wprowadzić jakąś formę abstrakcji w DAO tak aby nie polegały one (tak jak to jest w tym momencie) bezpośrednio na driverze H2

3 odpowiedzi do “News Radar: Play Framework, Slick, Dependency Injection i podział na moduły”

    1. Dzięki Maciek :)

      Jednym z założeń tego projektu było wzięcie na warsztat wielu różnych technologii, z których albo na codzień nie korzystam w ogóle, albo miałem bardzo mały kontakt. W tym momencie pojawia się problem – bo z jednej strony dokumentacja do Play’a, Slick’a czy Sbt wydaje się być bardzo dobra – ale jakbym miał najpierw po kolei wszystko czytać, to pewnie zabrałbym się za aplikację za jakieś kilka miesięcy :) Z drugiej strony szukanie rozwiązań konkretnych problemów, kiedy nie znam „bigger picture” danej technologii jest znacznie cięższe.

      Dlatego też chcę opisywać rzeczy które chcę osiągnąć – jakie przeszkody mi stawały na drodze i koniec końców jak je rozwiązałem. Będzie to notatka dla mnie na przyszłość – ale liczę na to, że inne osoby które staną przed podobnymi problemami będą mogły znaleźć rozwiązanie szybciej dzięki tym wpisom.

      Przykładowo – strasznie dużo czasu zeszło mi na odkryciu dlaczego moduły sbt nie widzą klas z innych modułów mimo, że mam zadeklarowaną zależność dependsOn wg tego co jest w dokumentacji – jestem pewien, że dla osoby, która z sbt jest obyta – problem byłby do rozwiązania natychmiast. Natomiast mi zdążyło to najpierw popsuć sporo krwi :)

      1. Wiem o czym piszesz, a szczególnie to odczuwam teraz, gdy zacząłem zabawy z Play. Wrzucam do projektu Ebean ORM, po czym doczytuje w dokumentacji, że mogę zastosować również Hibernate i zmieniam od początku:)

        Aktualnie nie mogę się doczytać czy model w Play 2.5.x mam dziedziczyć po play.db.jpa.Model dla JPA czy już nie. Bo zgodnie z dokumentacją w EBean dziedziczę po play.db.ebean.Model, ale dla Hibernate już nie napisali, a u mnie nie widzi tego importu choć mam zależności w build.sbt..

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *