Часть 12. Akka-Http. Поточный JSON. Условные кеш и условные запросы.

Страничка курса: https://maxcom.github.io/scala-course-2020/

План

  1. Akka-http
  2. Роутинг
  3. Сериализация/десериализация
  4. Поточный JSON
  5. Управление кешированием
  6. Условные запросы
  7. Итоги по домашнему заданию

Сегодня последняя лекция курса

Akka-http

Совмененный асинхронный HTTP сервер и клиент.

(основное использование - серверная сторона)

Построен на технологиях Akka.

Асинхронность - Future и Akka Streams.

Построен на:

  • akka-io - неблокирующийся ввод-вывод
  • akka-streams - реактивные потоки

Начнем с Hello, world!

build.sbt


libraryDependencies +=
  "com.typesafe.akka" %% "akka-http" % "10.1.11"

// версию Akka выбираем сами (>= 2.5)
libraryDependencies +=
  "com.typesafe.akka" %% "akka-stream" % "2.6.4"
					

Импортируем scaladsl


import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
					

(не путаем с Java API)

Запуск сервера

// нужна "классическая" система
implicit val system: ActorSystem = ActorSystem("my-system")

// обработчик запросов
val route: Route = ???

val bindingFuture: Future[Http.ServerBinding] = 
  Http().bindAndHandle(route, "localhost", 8080)
					

val route: Route =
  path("hello") { // путь "/hello"
    get { // метод "GET"
      complete(
        HttpEntity(
          ContentTypes.`text/html(UTF-8)`, 
	  "

Say hello to akka-http

")) } }

Обработчик запросов


type Route = RequestContext => Future[RouteResult]
// неявно преобразоввывается в Flow из akka-streams
// Flow[HttpRequest, HttpResponse, Any]
					

Альтернативный роутинг:

Стандартный роутинг (плюсы):

  • Максимально гибкий, API любой сложности
  • Легко расширяется
  • Неограниченная композиция

Стандартный роутинг (минусы):

  • Переусложнен
  • IDEA тормозит
  • Ручное форматирование кода
  • Нет генерации документации

Роутинг


type Route = RequestContext => Future[RouteResult]
					

RouteResult - ADT

  • Complete(response: HttpResponse)
  • Rejected(rejections: Seq[Rejection])

Directive - элемент роутинга


abstract class Directive[L](implicit val ev: Tuple[L]) {
  // tuple-apply
  def tapply(f: L => Route): Route

  ... // функции модификации и композиции
}
					

Directive может:

  • Модифицировать запрос
  • Фильтровать запросы по условию
  • Извлекать данные из запроса и контекста
  • Модифицировать ответ
  • Завершать ответ
  • Directive0 - без параметров
  • Directive1 - с 1-м параметром
  • Directive2 - с 2-мя
  • ...

Пример:


path("hello") {
  get { // Directive0 требует метод GET
    complete("Say hello to akka-http")
  }
} ~  // Directive1 требует путь "order/$id"
path("order" / IntNumber) { id => // извлеченный id
  complete(s"Order $id")
}
					

Композиция:


path("order" / IntNumber) { id =>
  (get | put) {
    extractMethod { m =>
      complete(s"Received ${m.name} request for order $id")
    }
  }
}
					

// собрем custom директиву
val orderGetOrPutWithMethod =
  path("order" / IntNumber) & (get | put) & extractMethod

val route =
  orderGetOrPutWithMethod { (id, m) =>
    complete(s"Received ${m.name} request for order $id")
  }

					

Работа с Future


val future: Future[String] = Future.successful("value")
val route =
  path("success") {
    onSuccess(future) { v => // Directive[Tuple1[String]]
      complete(s"Future was completed with $v.")
    }
  }
					

Примеры директив будут во второй половине

Сериализация


def complete(m: => ToResponseMarshallable): StandardRoute
// implicit преобразование
					

typeclass напрямую не используется


type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]

sealed abstract class Marshaller[-A, +B] {
  def apply(value: A)(implicit ec: ExecutionContext): 
    Future[List[Marshalling[B]]]
  ...
}
					

возможно несколько вариантов маршаллинга
(content negitiation)

JSON: akka-http-json

  • Argonaut
  • avro4s
  • AVSystem GenCodec
  • circe
  • Jackson
  • Json4s
  • jsoniter-scala
  • Play JSON
  • uPickle

build.sbt


libraryDependencies +=
  "de.heikoseeberger" %% "akka-http-play-json" % "1.31.0"
					

import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._

...

  complete(Json.obj("value" -> "Hello, world!"))

...
					

Десериализация


case class Data(value: String)
object Data {
  implicit val format: OFormat[Data] = Json.format[Data]
}
					

get {
  complete(Data("Hello, world"))
} ~ post {
  // entity - Directive1
  // as[Data] ищет implicit FromRequestUnmarshaller[T]
  entity(as[Data]) { data =>
    complete(s"Got ${data.value}")
  }
}
					

Поточный JSON

Варианты:

  • Массив объектов (*)
  • Объекты разделенные запятой или пробелом (*)
  • Объекты без перевода строки разделенные переводом

(*) готовая реализация

Чтение: обычно в клиенте или из файла/БД. Но бывает и для тела запроса:


implicit val jsonStreamingSupport: JsonEntityStreamingSupport =
  EntityStreamingSupport.json()

entity(as[Source[Data, _]]) { data =>
					

Используем вне routing dsl:


Flow[ByteString]
  .via(jsonStreamingSupport.framingDecoder)
  .mapAsync(1)(bytes => Unmarshal(bytes).to[Data])
					

Разбиваем по переводу строки


// вместо jsonStreamingSupport.framingDecoder
Framing.delimiter(
  ByteString("\r\n"), 
  maximumFrameLength = 16384, 
  allowTruncation = false)
					

Запись JSON


get {
  val source: Source[Data, NotUsed] = 
    Source(Seq(Data("value")))
  complete(source)
}
					

Проблема - обработка ошибок

  • Future - результат или error page
  • Source - оборванный stream

Ошибки вместо пустых ответов:


def dataOrFail[T]
    (source: Source[T, NotUsed]): Future[Source[T, NotUsed]] = {
  source
    .prefixAndTail(1)
    .runWith(Sink.head) 
    .map { case (first, rest) =>
      Source(first).concat(rest)
    }
}
					

(future падает при ошибке получения 1-го элемента)

Управление кешированием

Кеширование - полезная "фича" HTTP.

Кешируются GET запросы без тела.

Поддерживается браузерами и многими http-клиентами.

Три варианта заголовков:

  • Явно выключено
  • Не указано
  • Явно включено

Не указано - на усмотрение клиента. Обычно:

  • Клиенты не кешируют
  • AJAX не кешируется (кроме IE)
  • HTML кешируется

если ожидаем странные клиенты то лучше всегда явно указывать

Выключение кеширования:


Cache-control: no-store, no-cache, must-revalidate
Pragma: no-cache
Expires: 0
					

respondWithHeader(
   `Cache-Control`(`no-store`, `no-cache`, `must-revalidate`)) {
  complete("Hello, world")
}
					

Кеширование с таймаутом


Cache-control: max-age=${seconds}
					

"Вечное" кеширование


Cache-control: max-age=365000000, immutable
					

Таймаут для меняющихся ответов должен быть очень коротким.

Кодируем "версию" в URL - иногда весьма эффективно.

Для REST можно комбинировать с redirect.

Условные запросы

Кеширование с проверкой актуальности кеша.

Когда это эффективно?

  • Большие ответы (например картинки)
  • "Тяжелые" запросы с возможностью быстрой проверки

Пример быстрых проверок:

  • Сохраненная дата последней модификации
  • Сохраненый номер версии

Помечаем ответы заголовком, на выбор:

  • Last-modified - дата
  • Etag - произвольная строка
    (номер версии, random, хеш-сумма)

// нестандартный DateTime из Akka Http
val lastModified: DateTime = ???
conditional(lastModified) { // 304 Not Modified если не изменился
  respondWithHeader(`Cache-Control`(`must-revalidate`)) {
    complete("Hello, world")
  }
}
					

Итоги по домашнему заданию

24 апреля - демо. Стартуем в 17:00.

Показываем свой проект через Zoom.

Примерно 10 минут - покажите UI и код, расскажите о достижениях и проблемах. 5 минут на вопросы.

Напоминаю: