Einführung in Akka Http Path Directives

Keine Kommentare

In vielen Web-Frameworks werden die Pfade einer Webanwendung mithilfe von speziellen Konfigurationsdateien oder per Annotations definiert. Akka bietet eine ausgezeichnete DSL, um Pfade programmatisch festzulegen. Dabei werden Prinzipien wie DRY und Separation of Concerns sehr gut unterstützt. In diesem Artikel sollen die dazu nutzbaren Direktiven und ihre Nutzung anhand eines kurzen Beispiels eingeführt werden.

Zu Beginn starten wir mit einem einfachen Server, der beim Aufruf von http://localhost:8080/my-path mit dem Text My-Path antwortet.

object Main extends App {
 implicit val system = ActorSystem()
 implicit val executor = system.dispatcher
 implicit val materializer = ActorMaterializer()
 
 val paths = {
   path("my-path") {
     complete("My-Path")
   }
 }
 
 Http().bindAndHandle(paths, "localhost", 8080)
 
 println(s"Server online at http://localhost:8080")
}

Wichtig ist dabei, dass der Aufruf exakt http://localhost:8080/my-path lautet – der Aufruf von http://localhost:8080/my-path/ ist nicht erfolgreich, sondern wird mit The requested resource could not be found. beantwortet.

Um den Server robuster zu gestalten, so dass beide Anfragen erfolgreich sind, ist eine kleine Anpassung der Pfad-Definition notwendig.

val paths = {
  pathPrefix("my-path") {
    pathEndOrSingleSlash {
      complete("My-Path")
    }
  }
}

Path-Direktiven lassen sich also sehr leicht kombinieren und verschachteln.

Ist der Pfad vollständig, lässt sich diese Pfadangabe auch kürzer schreiben.

val paths = {
   (pathPrefix("my-path") & pathEndOrSingleSlash) {
       complete("My-Path")
   }
 }

Da für ähnliche Konstellationen Code wiederholt werden würde, lassen sie sich derartige Kombinationen zur Wiederverwendung auslagern.

trait ConcisePaths {
 
  def concisePath(segment: String) = {
    pathPrefix(segment) & pathEndOrSingleSlash
  }
}

Im Trait ConcisePaths ist die Kombination aus pathPrefix und pathEndOrSingleSlash bereitgestellt und kann bei der Definition der Pfade verwendet werden.

object Main extends App with ConcisePaths {
  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()
 
  val paths = {
    concisePath("my-path") {
        complete("My-Path")
    }
  }
 
  Http().bindAndHandle(paths, "localhost", 8080)
 
  println(s"Server online at http://localhost:8080")
}

Eine genauere Auswertung des Requests ist weiterhin möglich. Hier wird nicht mehr jede Request-Methode gleich behandelt, sondern zwischen POST und GET unterschieden. Die jeweiligen Direktiven werden dabei mit ~ im Block der Pfad-Behandlung miteinander konkateniert.

val paths = {
  concisePath("my-path") {
    get {
       complete("GET My-Path")
    } ~
    post {
        complete("POST My-Path")
    }
  }
}

Darüber hinaus lassen sich default- bzw. globale Behandlungen definieren. Der folgende Code gibt den Text DEFAULT My-Path aus, wenn die Anfrage nicht mit GET oder POST erfolgt.

val paths = {
   concisePath("my-path") {
      get {
        complete("GET My-Path")
      } ~
      post {
        complete("POST My-Path")
      } ~
      complete("DEFAULT My-Path")
    }

Bisher kann unser Beispiel-Server nur auf einen Pfad reagieren. Das Hinzufügen weiterer Pfad-Definitionen ist ebenfalls möglich, indem ein weiterer Pfad konkateniert wird.

  val paths = {
    concisePath("my-path") {
      get {
        complete("GET My-Path")
      } ~
      post {
        complete("POST My-Path")
      } ~
      complete("DEFAULT My-Path")
    } ~
    path("my-other-path1") {
      complete("My-Other-Path1")
    }

Da bei vielen Pfaden die Übersicht verloren gehen kann, kann die Pfade-Definition ganz oder teilweise ausgelagert werden, bspw. in einen Trait: Im Trait OtherPaths wird der Pfad my-other-path2 gesetzt. Die Behandlung des Pfades erfolgt mit oder ohne Slash am Ende. Außerdem sind zwei untergeordnete Pfade definiert, Alle Behandlungen innerhalb des Blocks von pathPrefix(„my-other-path2“) sind mit der Tilde verknüpft. Während der erste untergeordnete Pfad exakt auf my-other-path2/sub1 festgelegt ist, behandelt der zweite sowohl Anfrage mit oder ohne Slash am Ende, da concisePath aus dem Trait ConcisePaths verwendet wird, der durch diesen Trait erweitert wird.

// OtherPaths.scala
trait OtherPaths extends ConcisePaths {
 
  val otherPathWithSubPaths = {
    pathPrefix("my-other-path2") {
      pathEndOrSingleSlash {
        complete("my-other-path2/")
      } ~
      path("sub1") {
          complete("sub1")
      } ~
      concisePath("sub2") {
        complete("sub2")
      }
    }
  }
}

OtherPaths wird anstelle von ConcisePaths verwendet und der value otherPathWithSubPaths wird als weiterer Pfad angehängt, um auf die ausgelagerte Path-Definition zuzugreifen.

//Main.scala
object Main extends App with OtherPaths {
  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()
 
 
 
  val paths = {
    concisePath("my-path") {
      get {
        complete("GET My-Path")
      } ~
      post {
        complete("POST My-Path")
      } ~
      complete("DEFAULT My-Path")
    } ~
    path("my-other-path1") {
      complete("My-Other-Path1")
    } ~
    otherPathWithSubPaths
  }
 
  Http().bindAndHandle(paths, "localhost", 8080)
 
  println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
}

Fazit

Mithilfe der Path-Directives lassen sich Pfade sehr leicht beschreiben und bspw. in passende Module auslagern. Mithilfe der DSL lassen sich die Pfade je nach Bedarf sowohl grob- als auch feingranular behandeln. Die DSL selbst lässt sich sehr leicht auf die jeweiligen Bedürfnisse anpassen sowie spezialisieren. Gemäß dem Prinzip DRY können diese Funktionalitäten ausgelagert werden.

Um tiefer einzusteigen, bietet die Akka Http Doku einen übersichtlichen Einstiegspunkt über die Routing-DSL.

Der Quellcode mit dem letzten Stand befindet sich in diesem Repo.

Christian Hof

Christian Hof beschäftigt sich mit Big Data und das am liebsten innerhalb des Hadoop-Ökosystems. Dabei spielen agile Methoden und Testautomatisierung eine wichtige Rolle.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.