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
регистрирует информацию об исходящих ответах и указывает, на каком уровне их регистрировать. Библиотека журнала должна быть включена для этого, чтобы работать. Вывод будет выглядеть примерно так:1INFO 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, являются их собственными. |