Статьи

Ktor — Kotlin веб-фреймворк

Ktor — это асинхронный веб-фреймворк, написанный и разработанный для Kotlin. Позволяя использовать более впечатляющие функции Kotlin, такие как сопрограммы, не только для использования, но и для поддержки в качестве первоклассного гражданина. Как правило, Spring — это мой общий подход и обычно то, что я использую, когда мне нужно собрать REST API вместе. Но после недавнего посещения лондонской встречи Kotlin, где была презентация о Ktor, я решил попробовать что-то новое на этот раз. Вот так я и оказался здесь, написав пост в блоге о Кторе. Таким образом, этот пост является опытом обучения для вас и меня. В содержании этого поста не будет опытных советов, но вместо этого будет документироваться мое путешествие, поскольку я впервые играю с Ktor.

Вот немного дополнительной информации о Ktor. Он поддерживается Jetbrains , которые также являются создателями самого Kotlin. Кто лучше сделает веб-фреймворк Kotlin, чем мужчины и женщины, работающие над языком.

Реализация

зависимости

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
buildscript {
  ext.kotlin_version = '1.3.41'
  ext.ktor_version = '1.2.2'
 
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}
 
apply plugin: 'java'
apply plugin: 'kotlin'
 
// might not be needed but my build kept defaulting to Java 12
java {
  disableAutoTargetJvm()
}
 
// Ktor uses coroutines
kotlin {
  experimental {
    coroutines "enable"
  }
}
 
compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
 
dependencies {
  // Kotlin stdlib + test dependencies
 
  // ktor dependencies
  compile "io.ktor:ktor-server-netty:$ktor_version"
  compile "io.ktor:ktor-jackson:$ktor_version"
  // logback for logging
  compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
  // kodein for dependency injection
  compile group: 'org.kodein.di', name: 'kodein-di-generic-jvm', version: '6.3.0'
}

Здесь происходит несколько вещей.

  • Ktor требует минимальной версии Kotlin 1.3 , так что сопрограммы могут быть использованы.
  • ktor-server-netty зависимости от ktor-server-netty и ktor-jackson . Как следует из названия, это означает, что Netty будет использоваться для этого поста. Различные базовые веб-серверы могут быть использованы в зависимости от того, что вы решили импортировать. В настоящее время оставшимися вариантами являются Jetty и Tomcat .
  • Logback введен для обработки регистрации. Это не входит в зависимости Ktor и необходимо, если вы планируете вести какие-либо записи.
  • Кодеин — это структура внедрения зависимостей, написанная на Kotlin. Я использовал его свободно в этом посте, и из-за размера примеров кода, я мог бы удалить его полностью. Основная причина этого в том, чтобы предоставить мне еще один шанс использовать что-то, кроме Spring. Помните, что это также одна из причин, по которой я пробую Ктор.

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

Теперь, когда все скучно, я могу помочь вам реализовать простой веб-сервер. Код ниже это все, что вам нужно:

1
2
3
4
5
6
7
fun main() {
  embeddedServer(Netty, port = 8080, module = Application::module).start()
}
 
fun Application.module() {
  // code that does stuff which is covered later
}

Bam. Там у вас есть это. Простой веб-сервер, работающий с Ktor и Netty. Хорошо, да, это на самом деле ничего не делает, но мы остановимся на этом в следующих разделах. Код довольно понятен. Единственное, что стоит выделить — это функция Application.module . Для параметра module embeddedServer требуется функция расширения для Application . Это будет основная функция, которая заставляет сервер делать вещи.

В следующих разделах мы расширим содержимое Application.module так, чтобы ваш веб-сервер действительно делал что-то стоящее.

Маршрутизация

На данный момент все входящие запросы будут отклонены, так как нет конечных точек для их обработки. Установив маршрутизацию, вы можете указать действительные пути, по которым могут проходить запросы, и функции, которые будут обрабатывать запросы, когда они достигнут своих пунктов назначения.

Это делается внутри блока Routing (или нескольких блоков Routing ). Внутри блока устанавливаются маршруты к различным конечным точкам:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
routing {
  // all routes defined inside are prefixed with "/people"
  route("/people") {
    // get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}

routing — это небольшая удобная функция, чтобы сделать поток кода немного более плавным. Контекст (он же) внутри routing имеет тип Routing . Кроме того, функции route , get и post являются функциями расширения Routing .

route устанавливает базовый путь ко всем последующим конечным точкам. В этом сценарии /people . get и post сами не указывают путь, так как базовый путь достаточен для их нужд. При желании, путь может быть добавлен к каждому, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
routing {
  // get a person
    get("/people/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
  // create a person
  post("/people) {
    val person = call.receive<Person>()
    val result = personRepository.save(person.copy(id = UUID.randomUUID()))
    call.respond(result)
  }
}

Прежде чем перейти к следующему разделу, я хочу показать вам, как я на самом деле реализовал маршрутизацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun Application.module() {
  val personRepository by kodein.instance<PersonRepository>()
  // route requests to handler functions
  routing { people(personRepository) }
}
 
// extracted to a separate extension function to tidy up the code
fun Routing.people(personRepository: PersonRepository) {
  route("/people") {
    // get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}

Я извлек код в отдельную функцию, чтобы уменьшить содержимое Application.module . Это будет хорошая идея, когда вы пытаетесь написать более значимое приложение. То, как я это сделал, — это путь Ктора или нет, это другой вопрос. Если взглянуть на документы Ktor, то это выглядит как приличное решение. Я считаю, что нашел другой способ сделать это, но мне нужно было бы проводить больше времени с этим.

Содержимое обработчика запросов

Код, который выполняется, когда запрос направляется обработчику запроса, очевидно, очень важен. Функция должна что-то делать в конце концов …

Каждая функция-обработчик выполняется в контексте сопрограммы. Я на самом деле не использовал этот факт, так как каждая из функций, которые я показал, полностью синхронна. Для получения дополнительной информации об этом в документах Ktor есть пример асинхронного выполнения.

В оставшейся части этого поста я постараюсь не упоминать сопрограммы слишком часто, поскольку они не особенно важны для этого простого REST API.

В этом разделе функция get будет рассмотрена чуть ближе:

1
2
3
4
5
6
get("/{id}") {
  val id = UUID.fromString(call.parameters["id"]!!)
  personRepository.find(id)?.let {
    call.respond(HttpStatusCode.OK, it)
  } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
}

{id} указывает, что в запросе ожидается переменная пути, и ее значение будет сохранено как id . Можно включить несколько переменных пути, но для этого примера требуется только одна 👍. Значение id извлекается из call.parameters который принимает имя переменной, к которой вы хотите получить доступ.

  • call представляет контекст текущего запроса.
  • parameters — список параметров запроса.

Используя id из переменных пути, база данных ищет соответствующую запись. В этом случае, если она существует, запись возвращается вместе с соответствующими 200 OK . Если это не так, возвращается ответ об ошибке. И respondText и respondText изменяют базовый response текущего call . Вы можете сделать это вручную, например, используя:

1
2
call.response.status(HttpStatusCode.OK)
call.response.pipeline.execute(call, it)

Вы можете сделать это, но в этом нет необходимости, поскольку это на самом деле просто реализация respond . respondText имеет некоторую дополнительную логику, но делегирует response чтобы завершить все. Последний вызов для execute в этой функции представляет собой возвращаемое значение функции.

Установка дополнительных функций

В Ktor дополнительные функции могут быть подключены при необходимости. Например, для обработки и возврата JSON из вашего приложения можно добавить разбор JSON Джексона . Ниже приведены функции, установленные для примера приложения:

1
2
3
4
5
6
7
fun Application.module() {
  install(DefaultHeaders) { header(HttpHeaders.Server, "My ktor server") }
  // controls what level the call logging is logged to
  install(CallLogging) { level = Level.INFO }
  // setup jackson json serialisation
  install(ContentNegotiation) { jackson() }
}
  • DefaultHeaders добавляет заголовок к каждому ответу с именем сервера.
  • CallLogging регистрирует информацию об исходящих ответах и ​​указывает, на каком уровне их регистрировать. Библиотека журнала должна быть включена для этого, чтобы работать. Вывод будет выглядеть примерно так:

    1
    INFO  ktor.application.log - 200 OK: GET - /people/302a1a73-173b-491c-b306-4d95387a8e36
  • ContentNegotiation указывает серверу использовать Джексона для входящих и исходящих запросов. Помните об этом, включая ktor-jackson в качестве зависимости. Вы также можете использовать GSON, если хотите.

Для длинного списка других функций, которые включает в себя Ktor, вот удобная ссылка на их документы .

Установка функций полностью связана с выполненной ранее маршрутизацией. routing делегатов для install внутри его реализации. Чтобы вы могли написать:

1
2
3
4
5
6
7
install(Routing) {
  route("/people") {
    get {
      // implementation
    }
  }
}

Что бы ни плавало на твоей лодке, но я бы просто использовал routing . Надеюсь, это помогло вам понять, что происходит под капотом, даже если это было совсем немного.

Краткое упоминание о Кодейне

Я хочу очень кратко взглянуть на Кодейна, так как я использовал его в этом посте. Кодеин — это структура внедрения зависимостей, написанная на Kotlin для Kotlin. Ниже приведено очень небольшое количество DI, которое я использовал для примера приложения:

1
2
3
4
5
val kodein = Kodein {
  bind<CqlSession>() with singleton { cassandraSession() }
  bind<PersonRepository>() with singleton { PersonRepository(instance()) }
}
val personRepository by kodein.instance<PersonRepository>()

Внутри блока Kodein экземпляры классов приложения. В этом сценарии требуется только один экземпляр каждого класса. Вызов singleton означает это. instance — это заполнитель, предоставленный Kodein для передачи в конструктор вместо реального объекта.

Вне блока PersonRespository извлекается экземпляр PersonRespository .

Да, я знаю, здесь нет особого смысла в использовании Кодейна, поскольку я мог бы заменить его одной строкой …

1
val personRepository = PersonRepository(cassandraSession())

Вместо этого, давайте подумаем об этом как о очень кратком примере, чтобы понять 👍.

Заключительные мысли

Как человек, который предвзято относится к Spring, я обнаружил, что работа с Ktor сильно отличается от того, к чему я привык. Мне потребовалось немного больше времени, чем обычно, чтобы разработать пример кода, которым я был доволен. Тем не менее, я думаю, что результат выглядит неплохо, и мне нужно будет потратить еще немного времени с Ktor, чтобы лучше понять, как извлечь из этого максимум пользы. На данный момент я уверен, что из Ktor можно выжать гораздо больше. Для получения дополнительной информации о Ktor, я должен буду снова отослать вас к их документации, где у них есть много образцов и учебных пособий.

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотреть оригинальную статью здесь: Ktor — веб-фреймворк Kotlin

Мнения, высказанные участниками Java Code Geeks, являются их собственными.