У нас в команде давно витала мысль: «А не переехать ли нам с Spring Boot на что-то полегче?». Стартовое время инстансов росло, memory footprint на 15 микросервисов превышал 32 GB на проде, а холодный старт в Kubernetes при scaling занимал 25–40 секунд. Я взял на себя задачу — оценить Micronaut как альтернативу и провести честный бенчмарк. Вот что вышло.
Архитектура Micronaut: compile-time DI
Главная разница между Micronaut и Spring Boot — подход к dependency injection. Spring использует runtime reflection для создания бинов: он сканирует classpath, читает аннотации и строит контекст при старте приложения. Micronaut же делает всё это на этапе компиляции через annotation processor. Фреймворк генерирует Java-код для DI-контейнера, и в runtime никаких отражений нет.
Последствия: Micronaut стартует быстрее, потребляет меньше памяти и совместим с GraalVM native image. Но есть и цена — compile-time обработка занимает больше времени при сборке, а некоторые библиотеки, которые полагаются на Spring-специфичные механизмы, не работают из коробки.
Бенчмарк: один и тот же REST-эндпоинт
Я реализовал один и тот же сервис на обоих фреймворках: CRUD для заказов с PostgreSQL, Flyway миграциями и Kotlin coroutines. Вот результаты:
Конфигурация: AWS c6i.xlarge (4 vCPU, 8 GB RAM), Java 21, Kotlin 2.0.21
Метрики:
Spring Boot 3.4 Micronaut 4.7
Старт (cold): 8.2 сек 1.4 сек
Старт (warm, JVM cached): 2.1 сек 0.6 сек
RSS memory (idle): 320 MB 95 MB
RSS memory (под нагрузкой): 580 MB 210 MB
Heap (max): 512 MB 256 MB
GraalVM native image: ~130 MB, 0.4 сек ~42 MB, 0.02 сек
Throughput (req/s): 12 400 15 800
p99 latency: 18 ms 12 ms
Размер JAR: 68 MB 22 MB
Цифры говорят сами за себя. Micronaut выигрывает по всем метрикам, кроме... экосистемы.
Код: один и тот же эндпоинт на обоих фреймворках
Spring Boot:
@RestController
@RequestMapping("/api/v1/orders")
class OrderController(
private val orderService: OrderService
) {
@GetMapping
suspend fun listOrders(
@RequestParam page: Int = 0,
@RequestParam size: Int = 20
): Page {
return orderService.findAll(PageRequest.of(page, size))
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
suspend fun createOrder(
@RequestBody @Valid request: CreateOrderRequest
): OrderDto {
return orderService.create(request)
}
@GetMapping("/{id}")
suspend fun getOrder(@PathVariable id: String): OrderDto {
return orderService.findById(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
}
Micronaut:
@Controller("/api/v1/orders")
class OrderController(
private val orderService: OrderService
) {
@Get
suspend fun listOrders(
@QueryValue(defaultValue = "0") page: Int,
@QueryValue(defaultValue = "20") size: Int
): Page {
return orderService.findAll(Pageable.from(page, size))
}
@Post
@Status(HttpStatus.CREATED)
suspend fun createOrder(@Body @Valid request: CreateOrderRequest): OrderDto {
return orderService.create(request)
}
@Get("/{id}")
suspend fun getOrder(id: String): OrderDto {
return orderService.findById(id)
?: throw NotFoundException("Order not found")
}
}
Как видите, код практически идентичен. Разница в аннотациях минимальна — @RestController заменяется на @Controller, @GetMapping на @Get, @RequestBody на @Body. Это хорошо — порог входа низкий.
Боли миграции
Реальность оказалась сложнее бенчмарков. Вот с чем мы столкнулись при миграции реального сервиса (не тестового CRUD, а сервиса с 40+ эндпоинтами):
Первое — Spring Security. В Micronaut используется своя система безопасности, и миграция SecurityFilterChain с кастомными фильтрами заняла две недели. Некоторые специфичные вещи (например, JWT с кастомными claims и conditional access) пришлось переписывать с нуля.
Второе — @Transactional. В Micronaut он работает, но нюансы другие. Нет TransactionTemplate для programmatic-транзакций, propagation behaviour отличается. Пришлось несколько раз отлаживать race conditions в concurrent-сценариях.
Третье — экосистема. Spring Data JPA в Micronaut не работает (есть Micronaut Data, но API другой). Spring Cloud (config, discovery, circuit breaker) — нет прямых аналогов, нужно собирать из разных библиотек. А вот если вы используете чистый JDBC или Exposed — migration прямая.
Сообщество и экосистема
Spring Boot — это индустриальный стандарт. На любой вопрос есть ответ на StackOverflow, документация исчерпывающая, а библиотеки — тысячи. Micronaut растёт, но разрыв заметен. На момент написания статьи для Spring Boot существует ~15 000 библиотек-стартеров, для Micronaut — около 200. Если вам нужна специфичная интеграция, есть шанс, что готового модуля нет.
Наше решение
Мы решили компромиссный путь: новые микросервисы с простой логикой (aggregators, gateways, lightweight workers) пишем на Micronaut. Сервисы со сложной бизнес-логикой и глубокой интеграцией со Spring-экосистемой оставляем на Spring Boot. Один сервис уже мигрировали — это наш API Gateway, который стал потреблять 120 MB вместо 450 MB, а стартовать за 0.8 секунды вместо 5.4. Для CI/CD pipeline это означало ускорение деплоя на 3 минуты.
Micronaut не убийца Spring Boot, и я не пытался доказать обратное. Это инструмент для задач, где каждый мегабайт памяти и каждая секунда старта на счету. Если у вас 5 монолитных сервисов — разницы не почувствуете. Если у вас 50+ микросервисов в Kubernetes с auto-scaling — разница в $5000-8000/месяц на инфраструктуру. И это уже аргумент.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье