Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

JSON mit Akka HTTP

22.11.2016 | 6 Minuten Lesezeit

In diesem Artikel wird die Nutzung von JSON mit Akka HTTP thematisiert.

Dabei verfolgen wir zunächst eine Umsetzung, die sich an der aktuellen Dokumentation von Akka orientiert und nutzen spray-json . Im Anschluss zeigen wir einen alternativen Ansatz mit akka-http-json und circe .

Die Domäne

Die Domäne wird hier bewusst einfach gehalten, da der Fokus auf Parsing und Mapping liegt:

1case class Customer(
2  id: UUID,
3  name: String,
4  registrationDate: LocalDate,
5  gender: Gender,
6  customerType: CustomerType,
7  addresses: Option[Set[Address]]
8)
9case class Address(
10  street: String,
11  city: String,
12  zip: String,
13  active: Boolean = false
14)

Neben diesen Case-Classes existieren noch die Aufzählungstypen Gender und CustomerType. Obwohl es nachteilig ist, Aufzählungen als Erweiterung von Enumeration zu implementieren (ein Artikel dazu hier ), wird CustomerType hier so umgesetzt, um alle Eventualitäten abzudecken. Ein Grund dafür kann bspw. der Umgang mit Legacy-Code sein.

1// Main.scala
2object CustomerType extends Enumeration {
3  type CustomerType = Value
4  val Regular, Vip = Value
5}

Gender wird als sealed Trait umgesetzt.

1sealed trait Gender
2case object Female extends Gender
3case object Male extends Gender

Server

Für das Beispiel existiert ein einfacher Server, dessen Routing durch paths definiert wird. POST-Requests werden unter http://localhost:8080/customer entgegengenommen. Das übertragene Objekt wird zunächst in der Konsole ausgegeben, anschließend customers hinzugefügt und schließlich wird die aktualisierte Liste hinzugefügt. GET liefert für eine bestimmte UUID das entsprechende Element der Map zurück.

1object Main extends App {
2  import domain._
3 
4  implicit private val system = ActorSystem()
5  implicit private val mat    = ActorMaterializer()
6 
7  // Some dummy data
8  private val uuid1 = UUID.fromString("5919d228-9abf-11e6-9f33-a24fc0d9649c")
9  private val uuid2 = UUID.fromString("660f7186-9abf-11e6-9f33-a24fc0d9649c")
10  private val uuid3 = UUID.fromString("70d0d722-9abf-11e6-9f33-a24fc0d9649c")
11 
12  private val address1 = Address("Musterstrasse 2", "Musterstadt", "12345")
13  private val address2 =
14    Address("Testplatz 80 5", "Musterhausen", "45789", active = true)
15  private val address3 =
16    Address("Akka-Allee 1887", "Akkaburg", "61860", active = true)
17 
18  private var customers =
19    Map(
20      uuid1 -> Customer(uuid1,
21                        "test1",
22                        LocalDate.of(2010, 1, 11),
23                        Female,
24                        CustomerType.VIP,
25                        None),
26      uuid2 -> Customer(uuid2,
27                        "test2",
28                        LocalDate.of(2014, 6, 5),
29                        Male,
30                        CustomerType.VIP,
31                        Some(Set(address1, address2))),
32      uuid3 -> Customer(uuid3,
33                        "test3",
34                        LocalDate.of(2012, 2, 25),
35                        Female,
36                        CustomerType.REGULAR,
37                        Some(Set(address3)))
38    )
39 
40  private def route = {
41    import Directives._
42    pathPrefix("customer") {
43      post {
44        entity(as[Customer]) { customer =>
45          println(customer)
46          customers += customer.id -> customer
47          complete(customers)
48        }
49      } ~
50      path(JavaUUID) { id =>
51        get {
52          complete(customers(id))
53        }
54      }
55    }
56  }
57 
58  import system.dispatcher
59  Http().bindAndHandle(route, "localhost", 8080).onComplete {
60    case Failure(cause) =>
61      println(s"Can't bind to localhost:8000: $cause")
62      system.terminate()
63    case _ =>
64      println(s"Server online at http://localhost:8080")
65  }
66  Await.ready(system.whenTerminated, Duration.Inf)

JSON-Support mit spray-json

Dieser Abschnitt orientiert sich an der Dokumentation zur Nutzung von JSON im Kontext von Akka HTTP 2.4.11. Um spray-json im Beispielprojekt einzubinden, wird folgende Dependency verwendet:

1libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json-experimental" % "2.4.11"

Um den Server JSON-fähig zu gestalten, bedarf es noch der Einbindungen der notwendigen Kapazitäten für spray-json sowie der Definition eines JSON-Protokolls. Dazu werden im Trait JsonSupport das JSON-Format für Address und Customer definiert. Ersteres wird mit dem Aufruf jsonFormat4(Address) erreicht – die vier steht für die Anzahl der Parameter im Konstruktor. Das Format für Customer wird äquivalent behandelt.

1trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
2  implicit val addressFormat = jsonFormat4(Address)
3  implicit val customerFormat = jsonFormat6(Customer)
4}

Der Trait JsonSupport wird in Main.scala hinein gemixt:

1object Main extends App with JsonSupport { //...

Custom-Types

Grundsätzlich würden diese Implementierungsschritte ausreichen, jedoch deckt DefaultJsonProtocol nicht alle Typen ab, die hier verwendet werden. Im vorliegenden Fall benötigen wir Formate für:

  • java.util.UUID
  • java.time.LocalDate
  • oben beschriebene Aufzählungen

Dazu wird das im Trait CustomerJsonProtocol für jedes benötige Format ein impliziter Wert des Types JsonFormat angelegt und dessen Methoden read und write implementiert.

1trait CustomerJsonProtocol extends DefaultJsonProtocol {
2 
3  implicit val uuidJsonFormat: JsonFormat[UUID] = new JsonFormat[UUID] {
4    override def write(x: UUID): JsValue = JsString(x.toString)
5 
6    override def read(value: JsValue): UUID = value match {
7      case JsString(x) => UUID.fromString(x)
8      case x           => deserializationError("Expected UUID as JsString, but got " + x)
9    }
10  }
11 
12  implicit val localDateJsonFormat: JsonFormat[LocalDate] =
13    new JsonFormat[LocalDate] {
14      private val formatter                     = DateTimeFormatter.ISO_DATE
15      override def write(x: LocalDate): JsValue = JsString(x.format(formatter))
16 
17      override def read(value: JsValue): LocalDate = value match {
18        case JsString(x) => LocalDate.parse(x)
19        case x           => deserializationError("Wrong time format of " + x)
20      }
21    }
22 
23  implicit val customerTypeFormat: JsonFormat[CustomerType] =
24    new JsonFormat[CustomerType] {
25      override def write(x: CustomerType): JsValue = JsString(x.toString)
26 
27      override def read(value: JsValue): CustomerType = value match {
28        case JsString("REGULAR") => CustomerType.REGULAR
29        case JsString("VIP")     => CustomerType.VIP
30        case x                   => deserializationError("No CustomerType with name " + x)
31      }
32    }
33 
34  implicit val genderFormat: JsonFormat[Gender] = new JsonFormat[Gender] {
35    override def write(x: Gender): JsValue = JsString(x.toString.toUpperCase)
36 
37    override def read(value: JsValue): Gender = value match {
38      case JsString("MALE")   => Male
39      case JsString("FEMALE") => Female
40      case x                  => deserializationError("Not a Gender " + x)
41    }
42  }
43}

Im Trait JsonSupport wird dieser Trait anstelle von DefaultJsonProtocol verwendet:

1trait JsonSupport extends SprayJsonSupport with CustomerJsonProtocol {//...

JSON mit akka-http-json und circe

Alternativ zu spray-json existieren weitere Bibliotheken, um JSON im Kontext von Akka HTTP zu nutzen. Eine davon ist akka-http-json, die hier mit circe verwendet wird. Dazu wird folgende Dependency (anstelle der Dependency für spray-json) eingebunden:

1libraryDependencies ++= List(
2  "de.heikoseeberger" %% "akka-http-circe" % "1.10.1",
3  "io.circe"          %% "circe-generic"   % "0.5.2",
4  "io.circe"          %% "circe-java8"     % "0.5.2"</ul>
5)

Die erste Dependency bindet die Integration von circe in Akka HTTP, die maßgeblich mittels CirceSupport zur Verfügung gestellt wird, ein. circe-generic ermöglicht die automatische Erzeugung von Codecs (analog Formaten bei spary-json) zur Compile-Time. circe-java8 beinhalten Codecs für spezielle Typen von Java 8, insbesondere LocalDate.

Um also circe zu nutzen, wird Main.scala folgendermaßen angepasst:

1// more imports
2import de.heikoseeberger.akkahttpcirce.CirceSupport
3 
4object Main extends App with CirceSupport {
5  import io.circe.generic.auto._
6  import io.circe.java8.time._
7  //...

Im Companion Object für CustomerType werden Decoder und Encoder als implizite Werte angelegt:

1object CustomerType extends Enumeration {
2  type CustomerType = Value
3  val Regular, Vip = Value
4 
5  implicit val customerTypeDecoder: Decoder[CustomerType] =
6    new Decoder[CustomerType] {
7      override def apply(c: HCursor) =
8        c.as[String].flatMap{ x =>
9          Xor
10            .catchOnly[NoSuchElementException](CustomerType.withName(x))
11            .leftMap(e => DecodingFailure(s"Error while decoding: ${e.getMessage}", List()))
12      }
13    }
14 
15  implicit val customerTypeEncoder: Encoder[CustomerType] =
16    new Encoder[CustomerType] {
17      override def apply(`type`: CustomerType) =
18        Json.fromString(`type`.toString)
19    }
20}

Für Gender wird analog vorgegangen. In der vorliegende Version (0.5.2) von circe, die hier verwendet wird, sollten ADTs automatisch, jedoch gab es hierbei Probleme, die nicht gelöst werden konnten. Außerdem weicht der JSON-Output ab, so dass ein eigener Codec benötigt wird. Mit kann circe 0.6 kann dieser aber konfiguriert werden.

1sealed trait Gender
2case object Female extends Gender
3case object Male   extends Gender
4 
5object Gender {
6 
7  implicit val genderDecoder: Decoder[Gender] =
8    new Decoder[Gender] {
9      override def apply(c: HCursor) = c.as[String].flatMap {
10        case "FEMALE" => Xor.right(Female)
11        case "MALE"   => Xor.right(Male)
12        case x        => Xor.left(DecodingFailure(s"Not a gender $x", List()))
13      }
14    }
15 
16  implicit val genderEncoder: Encoder[Gender] =
17    new Encoder[Gender] {
18      override def apply(a: Gender) = Json.fromString(a.toString.toUpperCase)
19    }
20}

Für java.util.UUID sind dank circe-java8 bereits Mechanismen vorhanden und müssen nicht wie bei spray-json implementiert werden.

Fazit

JSON kann mit wenig Aufwand in einem auf Akka HTTP basierenden Server eingebunden werden. Ob nun spray-json, circe oder eine andere Bibliothek verwendet wird, hängt nicht nur vom Gusto des Entwicklers ab, sondern selbstverständlich von der Qualität, dem Funktionsumfang sowie wie die Pflege und Weiterentwicklung der jeweiligen Bibliothek. Persönlich erscheint circe umfangreicher als spray-json zu sein.

Das Repository mit den Projekten für circe und spray-json befindet sich hier .

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.