Когда мы начали переводить бэкенд с Java на Kotlin, первым вопросом стал выбор фреймворка. Spring Boot — привычный, проверенный, с огромной экосистемой. Ktor — новый, быстрый, написанный на Kotlin от начала до конца. Я проработал с обоими больше года на реальных продакшен-проектах, и вот что могу сказать.
Это не статья «что лучше» — это честное сравнение с конкретными метриками, примерами кода и граблями, на которые я наступил.
Spring Boot на Kotlin: удобно, но с нюансами
Spring Boot — это industrial standard для Java-бэкенда. Он работает на Kotlin, но философски остаётся Java-фреймворком. Все концепции — бины, контекст, инверсия управления — пришли из мира Java. Аннотации @RestController, @Autowired, @Configuration привычны любому Java-разработчику.
@RestController
@RequestMapping("/api/v1/products")
class ProductController(
private val productService: ProductService,
private val productMapper: ProductMapper
) {
@GetMapping("/{id}")
fun getProduct(@PathVariable id: Long): ResponseEntity<ProductDto> {
val product = productService.findById(id) ?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(productMapper.toDto(product))
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createProduct(@Valid @RequestBody request: CreateProductRequest): ProductDto {
return productMapper.toDto(productService.create(request))
}
}
Плюсы Spring Boot на Kotlin очевидны: огромная экосистема (Spring Security, Spring Data, Spring Cloud), готовые решения для любых задач, массовость — любой вопрос уже задавали на Stack Overflow. Конструктор инъекции через primary constructor работает отлично, и Kotlin убирает бойлерплейт геттеров/сеттеров.
Минусы — те, что вы ожидаете. Аннотации — это рефлексия. Cold start в Spring Boot на Kotlin составляет 2-4 секунды, даже для простого REST API. Размер базового JAR-файла — 25-40 МБ. А shadow JAR с зависимостями — 60-100 МБ. Для serverless (AWS Lambda) это критично: каждые 100 МБ увеличивают время холодного старта.
Spring Boot + Kotlin Coroutines
Начиная с Spring Boot 3.2+, корутины поддерживаются нативно. Вы можете пометить методы контроллера suspend и вернуть Flow:
@GetMapping("/products/stream")
fun streamProducts(): Flow<ProductDto> = productService.streamAll()
@GetMapping("/products/{id}")
suspend fun getProduct(@PathVariable id: Long): ProductDto {
return productMapper.toDto(productService.findById(id)!!)
}
Но под капотом Spring оборачивает корутины в Reactive Types (Mono/Flux), что добавляет слой абстракции. Это работает, но не даёт полной производительности, которую можно получить с нативно-корутинным фреймворком.
Ktor: Kotlin-нативный подход
Ktor — фреймворк, написанный JetBrains на Kotlin с нуля. Он использует корутины на всех уровнях: каждый запрос обрабатывается в отдельной корутине, и неблокирующий I/O — это базовая модель, а не опция.
fun Application.configureRouting() {
val productService = ProductService(ProductRepository())
routing {
route("/api/v1/products") {
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest)
val product = productService.findById(id)
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(product)
}
post {
val request = call.receive<CreateProductRequest>()
val product = productService.create(request)
call.respond(HttpStatusCode.Created, product)
}
}
}
}
Первое, что бросается в глаза — никаких аннотаций. Вся конфигурация через DSL-блоки. Это чище, типобезопаснее, и IDE понимает структуру маршрутов. Нет «магии» — вы видите, что происходит в каждом обработчике.
Ktor модульный: вы подключаете только то, что нужно. ContentNegotiation для сериализации, Authentication для авторизации, CORS — каждый функционал через плагин. Нет гигантского контекста приложения с сотней бинов, которые вы не использовали.
Время старта и размер
Реальные замеры для REST API с 15 эндпоинтами, PostgreSQL-подключением и JWT-авторизацией:
Метрика | Spring Boot 3.2 | Ktor 2.3
------------------------|-------------------|------------
Cold start (JVM) | 3.2s | 0.8s
Shadow JAR size | 78MB | 32MB
RSS (простой API) | 180MB | 65MB
Throughput (rps) | 12,000 | 18,000
P99 latency (1ms req) | 4ms | 1.5ms
TTFB (Time to First Byte)| 8ms | 2ms
Ktor стартует в 4 раза быстрее, занимает в 2.5 раза меньше памяти и обрабатывает на 50% больше запросов в секунду. Для микросервисов с частыми деплоями и serverless-функций разница колоссальная.
DI: Spring vs Kodein vs Koin
Spring Boot поставляется с собственным DI-контейнером. Ktor не навязывает DI — вы выбираете сами. Популярные варианты: Koin (лёгкий, на основе DSL), Kodein (похож на Dagger), или ручной injection через фабрики.
// Koin DI с Ktor
val appModule = module {
single { DatabaseConnection.environment() }
single { ProductRepository(get()) }
single { ProductService(get()) }
}
fun Application.module() {
install(Koin) {
modules(appModule)
}
routing {
val productService: ProductService by inject()
// ...
}
}
Koin проще Spring DI, но менее мощный. Нет scoped beans, нет lazy initialization с proxy, нет автоматической конфигурации через @ConditionalOnProperty. Для простых микросервисов Koin — отличный выбор. Для сложных систем с множеством конфигураций Spring DI удобнее.
Тестирование
Spring Boot Test — это швейцарский нож: @WebMvcTest, @DataJpaTest, @SpringBootTest — готовые аннотации для любого уровня тестирования. MockMVC для тестирования контроллеров, Testcontainers для интеграционных тестов с настоящей БД.
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@MockkBean
lateinit var productService: ProductService
@Test
fun `should return product by id`() {
every { productService.findById(1L) } returns Product(1L, "Widget", BigDecimal("29.99"))
mockMvc.perform(get("/api/v1/products/1"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.name").value("Widget"))
}
}
Ktor предлагает testApplication — встроенный тестовый движок:
class ProductRouteTest {
@Test
fun `should return product by id`() = testApplication {
application {
configureRouting()
install(ContentNegotiation) { json() }
}
val response = client.get("/api/v1/products/1")
assertEquals(HttpStatusCode.OK, response.status)
}
}
Ktor-тесты проще в настройке, но Spring Boot даёт более структурированный подход к тестированию на разных уровнях. Для unit-тестов разницы нет — обе экосистемы поддерживают JUnit 5 и MockK.
Экосистема и комьюнити
Это elephants in the room. Spring Boot — гигантская экосистема: Spring Security (OAuth2, JWT, RBAC), Spring Data (JPA, MongoDB, Redis), Spring Cloud (Service Discovery, Circuit Breaker, Config Server), Spring Actuator (метрики, health checks). Всё это «из коробки» с минимальной конфигурацией.
Ktor меньше, но активно растёт. Для базовых задач (аутентификация, сериализация, CORS, rate limiting) есть готовые плагины. Но для сложных сценариев (distributed tracing, service mesh, circuit breaking) придётся собирать решение из разных библиотек или писать свои.
Размер комьюнити — в пользу Spring Boot. Stack Overflow, блогов, курсов по Spring в десятки раз больше. Если у вас проблема со Spring — скорее всего, решение уже найдено. С Ktor иногда приходится разбираться в исходниках.
Мой вердикт
Для нового микросервиса на Kotlin без легаси-зависимостей от Spring — я выбираю Ktor. Нативная поддержка корутин, скорость, минимальный footprint и лаконичный DSL стоят того, чтобы привыкнуть к другому подходу. Мы перевели 6 микросервисов с Spring Boot на Ktor за последние 8 месяцев, и средний деплой ускорился с 45 секунд до 12.
Spring Boot — правильный выбор, если у вас уже есть Java-инфраструктура, команда привыкла к Spring-экосистеме, или вам нужны сложные Enterprise-фичи (Spring Security + OAuth2 + LDAP, Spring Cloud с десятками сервисов, распределённые транзакции через Spring Batch).
И главное: не стоит бояться смешивать оба фреймворка в одной инфраструктуре. У нас gateway на Spring Cloud Gateway (потому что Netflix/Zuul-интеграция), а бизнес-микросервисы — на Ktor. Это нормально.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье