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 12java { disableAutoTargetJvm()}// Ktor uses coroutineskotlin { 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 codefun 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регистрирует информацию об исходящих ответах и указывает, на каком уровне их регистрировать. Библиотека журнала должна быть включена для этого, чтобы работать. Вывод будет выглядеть примерно так:1INFO ktor.application.log -200OK: 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, являются их собственными. |