Play, Scala, sbt, Shading

Buyuk resmi gor yigenim.


Gelin birlikte sifirdan bir scala ve sbt kullarak, multi project bir yapida Play projesi olusturalim.

1. Play 

Play, JVM'de kabul gormus bir web application framework'u. Ilk baslangicta, kendi basina calisacak cok basit bir Play projesi olsutralim. IntelliJ kullaniyorum, yeni bir sbt projesi  olustuyorum. Daha sonra build.sbt uzerinde su sekilde eklemeler yapacagiz:

    name := "play-standalone"
    version := "0.1"
    scalaVersion := "2.13.8"

   lazy val root = (project in file("."))
    .enablePlugins(PlayScala)
    .settings(
    name := """cok sukseli api projesi""",
    organization := "com.lombak",
    version := "1.0-SNAPSHOT",
    scalaVersion := "2.13.6",
    libraryDependencies ++= Seq(
    guice,
    "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test
    ),
    scalacOptions ++= Seq(
    "-feature",
    "-deprecation",
    "-Xfatal-warnings"
    )
    )

Plugin eklememiz gerekiyor. Bunun icin  project klasorunde, plugins.sbt isimli bir dosya  olusturup, icerigini su sekilde degistiriyorum:

    addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.13")

Daha sonra sbt toolbarindan, projeyi guncelliyorum ki yaptigimiz degisikler hayata gecsin. Bu islem biraz surebilir neticede birsuru library download edilecek. 

Controller
Simdi Play dependency'lerini eklemis olduk. Peki play nasil calisiyor? 

Bir play projesinde bazi klasorler olmasi gerekiyor. Bunlardan ilki app klasoru. Model, View ve Controller 'lerimizi app klasorune koyuyoruz. (Every play MVC patternini kullaniyor). 

app klasrounu olusturduktan sonra, icerisinde de controllers isimli bir package ekliyorum. Ve bu package icerisinde HomeController.scala isimli bir scala class olusturup, icerigini su sekilde degistiriyorum:

    package controllers

    import javax.inject.{Inject, Singleton}
    import play.api.libs.json.{JsValue, Json}
    import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request}

    @Singleton
    class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

    def index() = Action { implicit request: Request[AnyContent] =>
    val json: JsValue = Json.parse("""
    {
    "kim" : "Lombak Sehidi"
    }
    """)
    Ok(json)
    }
    }

Bir diger olmazsa olmaz klasor ise conf klasoru ki, Play uygulamasi icin tum configler buradan yapiliyor. conf isimli bir klasor (root'ta) olsuturup, icerisine application.conf ve routes isimli iki dosya olusturuyorum. Simdilik application.conf bos kalacak ama routes dosyasinda enazindan 1 tane test amacli route tanimlayalim. routes dosyasini aci su sekilde degistiriyorum:

    GET / controllers.HomeController.index()

Boylece ne yapmis olduk? Root'a gelen (/) request'i HomeController'in index metoduna (Action) yonlendirmis olduk.

Simdi tek yapmamiz gereken, komut satirindan 

    sbt run

komutunu calistirmak. Daha sonra gorecegiz ki, Play, port 9000 uzerinde calismaya baslayacak. localhost:9000 'a giderseniz, denemelik json responsumuzu gorebilirsiniz. 

Bu arada belirtmek lazim ki Play calisabilmesi icin Java 8 ya da 11 gerekiyor. Diger bir surum var ise, problem cikabilir. 


2. Ikinci Proje

Esas business logic'i, play uygulamasi icerisinde tutmak istemiyorum. Bunu decouple edecegiz.  Tamam bu metod bir leaky abstraction ama simdilik idare edebiliriz. 

Yani, business logic baska bir projede yer alacak. Biz bu projeyi Play projesine import edecegiz ve sadece belirli interface'ler uzerinden etkilesime gececegiz. Ileride ornegin rest api degil de, bir cli yazmak isterseniz, tek yapmaniz gereken bussiness logic'i barindiran uygulamayi kullanan bir baska client yazmak olacak.

sbt projesinde bir root bir de sub-projeler bulunur. Play projesini hala daha root olarak tutmakta ben bir sikinti gormuyorum. Ama ikisi de sub-project seklinde de konumlandirilabilir. 

Bu yuzden sadece business logic barindiracak proje icin bir klasor olusturuyorum ve onceki kisimlara dokunmuyorum. 

build.sbt 'de de yeni bir sub-project ekleyelim ve de mevcut projeyi bu yeni sub-project'e depend edelim.

    lazy val root = (project in file("."))
    .enablePlugins(PlayScala)
    .settings(
    name := """cok sukseli api projesi""",
    organization := "com.lombak",
    version := "1.0-SNAPSHOT",
    scalaVersion := "2.13.6",
    libraryDependencies ++= Seq(
    guice,
    "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test
    ),
    scalacOptions ++= Seq(
    "-feature",
    "-deprecation",
    "-Xfatal-warnings"
    )
    )     .dependsOn(businessLogicProject)     .aggregate(businessLogicProject)

    lazy val businessLogicProject = (project in file("./businessLogic-project"))         .settings(
            name := """Cok acayip isler""",
        organization := "com.lombak",
        version := "1.0-SNAPSHOT",
        scalaVersion := "2.13.6",
        libraryDependencies ++= Seq(
        
// birtakim dependencyler
        )
        )

Simdi tekrardan sbt reload yapar isek, taslar yerine oturacaktir.

Hala daha, sbt run yaptigimizda Play framework'lu rest api calismaya baslayacak cunku kendisi hala root proje konumunda, guzel.

Simdi denemelik bir business logic class'i


3. Davetsiz Misafir

Hersey tam sorunsuz giderse zaten bu meslekten tat alamayiz. Bu noktada, business logic icerisinde kullandigim bir kutuphane (Apache OAK) ile Play framework arasinda google.guava dependency uzerinden bir conflict yasandi. 


Bu conflict neden yasandi? Cunku Apache OAK, guava 15 versiyona ihtiyac duyuyor. Ama Play, guava 30'a ihtiyac duyuyor. Malesef ki ana projenin tum dependency'leri ayni yerde bulunmak zorunda oldugu icin (Java'nin sucu?), guava 15 veya guava 30'dan birisi secilmek zorunda.

Sbt'nin default conflict resolver'i tabi ki daha yeni olan guava 30 versiyonu seciyor. Bu durumda da Apache OAK calismiyor cunku guava 15 ve 30 surumleri binary compatible degil. 

Bunu daha iyi gorebilmek icin sbt evicted komutunu calistirabiliriz. 30 ve 15 arasindan, 30'un secilmesi eviction olarak adlandiriliyor. Yani guava 30, guava 15'i defediyor.

[warn] Found version conflict(s) in library dependencies; some are suspected to be binary incompatible:[warn]  * com.google.guava:guava:30.1.1-jre is selected over {27.1-jre, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0}


Bu arada farkediyoruz ki guava 30 disinda 27.1'a depend eden baska dependencyler de varmis. 

Bu cok sik karsilasilan bir JVM-development problemi. Cozume bakacak olursak, cogu kisi shading metodunu onermis. En temelde, bir kutuphanenin adini degistirmek olarak basitlestirebiliriz ki bu sayede ayni kutuphanenin baska versiyonlari ile cakismiyor ve ikisi ayni anda kullanilabiliyor. 

Ancak Shading, sadece assembly asamasinda uygulanabiliyor. Yani bir projeyi assemble ederken, istenilen dependency'leri shade ederek isimlerini degistirebiliyoruz ve ayni fat jar icerisinde bunlari yan yana barindirabiliyoruz. 

Ama bizi assembly kurtarmiyor cunku Play framework ile gelistirdigimiz rest katmanindan, diger projedeki Apache OAK ile alakali fonksiyonaliteyi de calistirmam gerekiyor. Yani runtime'da bize shading gerekli. 

1. Wrapping Approach

Arastirmalar sonucunda gordum ki, bu noktada uygulanan bir yontem, sadece Apache OAK barindiran bir proje yaratarak bunu assemble etmek, ederken de guava dependency'sini shade etmek. Daha sonra olusacak olan Jar dosyasini, Play ile gelistirdigimiz projeye import etmek. Bu sayede guava 30 ile shade edilmis guava 15 ayni classpath icerisinde yeralabilecek ve cakisma olmayacak. 

Ayrica bu wrap etme islemi de otomatize edilebilir ve manual hic bir isleme gerek kalmaz. 

Ilk etapta bir projeyi sbt ile assemble edip fat jar olsuturabilmek  icin sbt-assembly plugini yuklememiz gerekiyor. Bunun icin root'taki project klasorune plugin.sbt isimli bir dosya olusturuyoruz:

    addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.13")

Daha sonra, build.sbt icerisinde oakDep isimli yeni bir proje olusturuyoum ki sadece oak dependencylerini barindiriyor. Esas onemli olan nokta, bu proje tanimlamasi icerisinde Shade operasyonunu cekmek:

lazy val oakDep = (project in file("./oak-dep"))
  .settings(
    scalaVersion := "2.13.6",
    assemblyJarName in assembly := name.value + ".jar",
    assemblyShadeRules in assembly := Seq(
      ShadeRule.rename("com.google.common.**" -> "baskaguava.@1").inAll
    ),
    ....

Son olarak fat jarimizi uretelim bakalim:

    sbt oakDep/assembly

Bu sayede, sadece bu jar icerisinde guava'nin namespace'i degismis olacak. Ayrica bu namespace'e referenas veren tum classlardaki referanslar da guncellenecek. Yani bir nevi kutuphanenin adini (ve de namespace'ini) degistirmis oluyoruz. Bunu soyle gorebiliriz:

    jar -tf oak-dep.jar | grep guava

    baskaguava/
    baskaguava/annotations/
    baskaguava/base/
    baskaguava/base/internal/
    baskaguava/cache/
    shadeguava/collect/
    baskaguava/escape/
    baskaguava/eventbus/
    ...


Son olarak da, bu jar icerisindeki class'lara ulasabilmek icin, hangi projeden ulasmak istiyorsam, o proje icerisinde bir lib/ klasoru olsuturup bu jar'i oraya kopyaliyorum. Bu sayede, sbt calismaya baslamadan once lib klasorundeki tum class'lari, o projenin classpath'ina ekleyecektir. 

Evet, bu approach belki calismaz diye 1 numarasi vermistim ama cok guzel calisti, cok da iyi calisti. Tum islem otomatize de edilebilir. Bu acidan en ufak bir amelelik, hacky feel vermiyor bana. 

Bu maceranin da sonuna bu sekilde gelmis oluyoruz arkadaslar. 

Stay hungry, stay foolish, stay healthy :D







  


Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding