Scala ve Akka Http ile Rest API -1
Serinin devamı amacıyla temel Scala bilgisi için önceki yazılara aşağıdaki linklerden ulaşabilirsiniz.
- Java’cılar Için Scala – 1 [Temel]
- Java’cılar Için Scala – 2 [Collections]
- Java’cılar Için Scala – 3 [OOP]
- Aktör Model Programlama
Bu yazıda Akka Http ve Scala ile basit Rest API yazmaya odaklanacağız.
Scala için IntelliJ kullanıyorum. sbt Scala’da build tool siz de yeni bir sbt projesi açarak başlayabilirsiniz.
Versiyonlarla ilgili sorun yaşadığım için aşağıdaki versiyonlar önerdiğim versiyonlar.
- scala : 2.13.7
- sbt : 1.5.8
- Akka : 2.6.8
- Akka Http : 10.2.7
Dependencies
1 2 3 4 5 6 7 8 9 10 11 12 |
val AkkaVersion = "2.6.8" val AkkaHttpVersion = "10.2.7" libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion, "com.typesafe.akka" %% "akka-stream" % AkkaVersion, "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion ) |
Bir önceki yazıda Actor‘lerden bahsetmiştik ve mailbox‘ları olduğunu söylemiştik. Actor class’ına gittiğimizde temelde aşağıdaki iki methodu göreceğiz. . final bir methodu override edemiyoruz ve receive methodu ise abstract method olduğu için override etmemiz gerek. Hatırlamadıysanız önceki yazıların üzerinden geçebilirsiniz.
1 2 3 4 5 6 7 |
final def sender(): ActorRef = context.sender() def receive: Actor.Receive |
ProductService adında bir Actor oluşturarak başlıyoruz ve Actor class’ından extends ediyoruz. Bu arada Actor class’ını incelerseniz bir trait olduğunu göreceksiniz. ActorLogging ise log’lama için kullanıyoruz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
case class Product(name: String, brand: String) object ProductDB{ case object FindAllProducts case class FindProduct(id: Int) case class AddProduct(product: Product) case class AddedProduct(id: Int) } class ProductService extends Actor with ActorLogging{ var productsList : Map[Int, Product] = Map() var productId: Int = 0 override def receive: Receive = { case FindAllProducts => log.info("finfing all products") sender() ! productsList.values.toList case FindProduct(id) => log.info(s"founding product by id: $id") sender() ! productsList.get(id) case AddProduct(product: Product) => log.info("adding new product") productsList += (productId -> product) sender() ! AddedProduct(productId) productId += 1 } } |
sender() methodunu görüyoruz Actor class’ı içerisinde olduğunu söylemiştik mesaj göndermek için !(ünlem). Buradaki ünlem ise aşağıdaki gibi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
trait ScalaActorRef { ref: ActorRef with InternalActorRef with ActorRefScope => /** * Sends a one-way asynchronous message. E.g. fire-and-forget semantics. * <p/> * * If invoked from within an actor then the actor reference is implicitly passed on as the implicit 'sender' argument. * <p/> * * This actor 'sender' reference is then available in the receiving actor in the 'sender()' member variable, * if invoked from within an Actor. If not then no sender is available. * <pre> * actor ! message * </pre> * <p/> */ def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit } |
Bir diğer adım ise server class’ımız. RestAPIServer isminde object oluşturuyoruz ve App’den extends ediyoruz. App bir Scala class’ı ve uygulamanın çalışması için gerekli main methodu içeriyor.
1 2 3 4 5 6 7 8 9 10 11 |
object RestAPIServer extends App{ implicit val system = ActorSystem("ProductRestAPI") implicit val materializer = ActorMaterializer() import system.dispatcher } |
Json Marshalling
1 2 3 4 5 6 7 8 9 |
trait ProductJsonProtocol extends DefaultJsonProtocol{ implicit val productFormat = jsonFormat2(Product) // jsonFormat 2 çünkü Product class'ı 2 parametreye sahip } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//JSON marshalling val product = Product("iphone 13 pro max" ,"Apple") println(product.toJson.prettyPrint) //JSON unmarshalling val stringProduct = """ |{ | "brand": "Apple", | "name": "iphone 13 pro max" |} |""".stripMargin println(stringProduct.parseJson.convertTo[Product]) |
Şimdi product listesi hazırlayıp bunu Product Actor’üne gönderelim ve ekleme yapalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
val productDb = system.actorOf(Props[ProductService],"productActor") // actor oluşturuyoruz val productList = List( Product("iphone 13","Apple"), Product("iMac","Apple"), Product("MacBook pro ","Apple"), Product("MacBook air ","Apple") ) productList.foreach { product => productDb ! AddProduct(product) } |
Şimdi de gelen isteği yakalaması için bir handler yazmamız gerek. mesaj göndermek için ! görmüştük şimdi de ? yani ask görüyoruz. Önceki yazıda da belirttiğimiz gibi Actor Model’de Java’daki gibi nesnenin state’ine direkt olarak erişmiyoruz. Actor’e soruyoruz ve belirttiğimiz timeout süresi içerisinde cevap bekliyoruz. AskSupport içerisinde detaylı implemantationa ulaşabilirsiniz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
implicit val defaultTimeOut = Timeout(2 seconds) val requestHandler: HttpRequest => Future[HttpResponse] = { case HttpRequest(HttpMethods.GET, Uri.Path("/api/products"),_,_,_) => val productFuture : Future[List[Product]]= (productDb ? FindAllProducts).mapTo[List[Product]] productFuture.map{ products => HttpResponse( entity = HttpEntity( ContentTypes.`application/json`, products.toJson.prettyPrint ) ) } case request: HttpRequest => request.discardEntityBytes() Future { HttpResponse (status = StatusCodes.NotFound) } } Http().bindAndHandleAsync(requestHandler, "localhost", 8080) |
Artık browser üzerinden belirtiğimiz path’e giderek sonuca ulaşabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// http://localhost:8080/api/products [ { "brand": "Apple", "name": "iphone 13" }, { "brand": "Apple", "name": "iMac" }, { "brand": "Apple", "name": "MacBook pro " }, { "brand": "Apple", "name": "MacBook air " } ] |
Bütün kod incelemek için;
https://gist.github.com/cemdrman/2392d7498c7d7c977a4b9c59b1bc2d9a
Faydalı olması dileğiyle.