Какое-то время назад я проводил сравнение JVM фреймворков для разработки backend-сервисов. Задача оказалась довольно амбициозной и затратной по времени, поэтому я ее так и не доделал до конца. Но часть вещей все же удалось сравнить из описать. Я решил, что не пропадать же тексту зря и публикую здесь кусок который касается набирающего сейчас популярного фреймворка - Ktor.
Довольно молодой игрок в команде старых и замшелых зрелых JVM фреймворков. Начать писать на нем web-приложения очень просто и для быстрого старта даже есть неплохая документация. Я бы сказал, что она покрывает все нужды начинающих и продолжающих, но из-за молодости самого фреймворка порой очень тяжело находить какие-то правильные практики его использования. Даже существующие на github-е примеры весьма неоднородны и используют ряд экспериментальных API. Туториалы найденные в гугле тоже не отличаются единством подходов, из-за чего мне пришлось несколько раз переписывать приложение при открытии новых граней этого фреймворка. Это неплохо в качестве развлечения, но когда я хочу построить большое приложение, то наличие какого-то референсного шаблонного проекта отвечающего на вопрос “сейчас принято делать так” очень бы помогло. В противном случае можно получить набор разных подходов в разных частях приложения, которые открывались разработчикам в ходе эволюции проекта.
В целом фреймворк выполнен в DSL стиле. Хорошо это или плохо я так и не смог однозначно для себя решить, но необходимость использовать вложенные блоки для добавления какой-нибудь функциональности показалась спорной. Вот так например выглядит добавление аутентификации к нескольким методам:
routing {
authenticate("myauth1") {
get("/authenticated/route1") {
// ...
}
get("/other/route2") {
// ...
}
}
get("/") {
// ...
}
}
Понравился функционал StatusPages
, который позволяет задать стандартные ответы на исключения. Очень удобно потом где-то в коде выкидывать исключение и быть уверенным, что пользователь получить адекватный ответ. Также эта штука может реагировать на HTTP коды.
Ну и самое важное наверное то, что Ktor можно назвать микрофреймворком. Со всеми плюсами и минусами отсюда вытекающими. В нем вы не увидите ORM или DI из коробки. Все это придется откуда-то брать, но при этом не все такие топики покрываются официальной документацией и примерами из официального репозитория. Поэтому в качестве DI было решено использовать Kodein. При чем скорее от безысходности, т.к. примера с Dagger 2, который по моему мнению лучше подходит для больших проектов в официальном репозитории не было, а то что удалось найти на github - выглядело странно и разбираться в этом было откровенно лень.
fun Application.libraryApp() = mainModule(Kodein {
bind<JwtService>() with singleton { JwtService() }
bind<PasswordHasher>() with singleton { PasswordHasher() }
})
fun Application.mainModule(kodein: Kodein, testing: Boolean = false) {
val jwtService by kodein.instance<JwtService>()
val passwordHasher by kodein.instance<PasswordHasher>()
...
}
Та же история с ORM. Официальных примеров, да и вообще каких-либо упоминаний в документации я не нашел, поэтому пришлось “выйти с этим вопросом в Интернет”. Там обнаружился полуподпольный ORM Exposed от JetBrains, документация к которому была еще более скудная чем к Ktor. Но с гуглом пополам удалось это втащить и завести. Надо сказать, что до выразительности Django ORM ему очень далеко. Для сравнения приведу пример с созданием связи многие ко многим (все примеры из официальной документации):
Django ORM
class Publication(models.Model):
title = models.CharField(max_length=30
class Article(models.Model):
headline = models.CharField(max_length=100)
publications = models.ManyToManyField(Publication)
Exposed
object Actors: IntIdTable() {
val firstname = varchar("firstname", 50)
val lastname = varchar("lastname", 50)
}
class Actor(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<Actor>(Actors)
var firstname by Actors.firstname
var lastname by Actors.lastname
}
object StarWarsFilmActors : Table() {
val starWarsFilm = reference("starWarsFilm", StarWarsFilms)
val actor = reference("actor", Actors)
override val primaryKey = PrimaryKey(starWarsFilm, actor, name = "PK_StarWarsFilmActors_swf_act") // PK_StarWarsFilmActors_swf_act is optional here
}
class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)
...
var actors by Actor via StarWarsFilmActors
...
}
Ну и там еще есть нюансы с созданием объектов при таких связях. Но даже без этих нюансов видна некоторая церемониальность ORM от JetBrains. При чем кажется, что дело тут далеко не в динамической природе python-а.
Отдельно стоит упомянуть написание тестов. Примеров мало, документации еще меньше и в этом очень тяжело разбираться. Какие-то тесты у меня написать получилось, но в итоге я их просто удалил, т.к. совершенно непонятно как тестировать ту или иную функциональность.
Завершить свой опыт с Ktor я решил упаковкой приложения в Docker с последующей “эмуляцией продакшена”. Тут претензий нет. Dockerfile из документации простой, понятный и заработал с первого раза. Был нюанс с описанием процесса создания fat-jar - описана очень старая версия Gradle Shadow Plugin, но больших проблем это не вызвало. Описания из репозитория плагина сняло все проблемы.
Итоговая оценка: 5 джанг из 10