Перейти к содержанию
V1 V2

Цена «Докторской по Spring»: что Kora пытается убрать из опыта работы с JVM-фреймворками

14 октября 2024

Эта статья — письменное сопровождение доклада, который ставит под сомнение, сколько знаний, специфичных для фреймворка, должен нести разработчик. В ней обобщаются аргументы доклада, измерения и роль Kora как контрпримера.

Существует привычный способ описывать зрелые фреймворки: их сложность трактуется как глубина, их долгая история — как накопленная мудрость, а годы, необходимые для освоения их граничных случаев, — как ценная экспертиза. Этот доклад намеренно оспаривает такое допущение. Его центральный вопрос — не в том, мощен ли Spring, популярен ли он, продуктивен ли и проверен ли в бою — он, безусловно, таков, — а в том, следует ли считать объём знаний, специфичных для фреймворка и необходимых для уверенной работы с ним, самостоятельной инженерной стоимостью.

Докладчик называет эту стоимость «Докторской по Spring». Фраза намеренно провокационна, но стоящая за ней идея конкретна. Ожидается, что разработчик в этой экосистеме понимает конструкторное и полевое внедрение, именование бинов, квалификаторы, колбэки жизненного цикла, семантику прокси, @Transactional, порядок аспектов, приоритет конфигурации, ослабленное связывание, кеширование тестового контекста, срезы (slices), авто-конфигурацию, оптимизацию запуска, компромиссы native-image и множество других деталей, которые не обязательно видны из самого кода приложения. Ни одна из этих тем по отдельности не является невозможной для изучения. Аргумент доклада в том, что их совокупный эффект становится значительным, когда умножается на разработчиков, команды, сервисы, конвейеры CI, производственные парки и годы сопровождения.

Kora появляется на протяжении всей презентации как контрпример. Он представлен не как «Spring, только быстрее», и докладчик не утверждает, что каждое приложение на Spring следует переписать. Более фундаментальная идея в том, что многие задачи фреймворка можно перенести с этапа выполнения на этап компиляции и представить как читаемый сгенерированный исходный код, а не как скрытые механизмы времени выполнения. Результирующая архитектура пытается снизить и операционные накладные расходы, и объём контекста фреймворка, который разработчики должны держать в голове.

Эта презентация относится к эпохе Kora 1.x, до текущего поколения Kora 2.0. Такая историческая привязка важна. Версия, обсуждаемая в докладе, была уже гораздо зрелее, чем Kora до 1.0, но Kora 2 с тех пор ещё больше упростила и консолидировала модель: синхронные контракты приложений на Java/Kotlin, исполнение с приоритетом виртуальных потоков, удаление реактивных и suspend-контрактов фреймворка, уточнённая обработка графа на этапе компиляции, более широкая документация и более целостный производственный стек. Принципы, обсуждаемые здесь — DI на этапе компиляции, явная композиция, тонкие абстракции, сгенерированный исходный код, прямой SQL и минимизация когнитивной нагрузки, специфичной для фреймворка, — остаются центральными.

Сам доклад намеренно конфронтационен. Некоторые заголовки слайдов используют такие фразы, как «Spring bullshit», «мы плачем» или «жертва оптимизации». Это риторические приёмы презентации, а не нейтральная терминология. Полезная статья-сопровождение должна сохранять техническую суть, отделяя риторику от доказательств. Сильнейшие части доклада — не оскорбления; это измерения и повторяющийся вопрос: что именно стоит эта абстракция, и кто за неё платит?

Стоимость начинается с самых простых функций фреймворка

Внедрение зависимостей — первый пример, потому что предполагается, что это самая простая для понимания функция. В современном коде на Spring конструкторное внедрение широко считается предпочтительным паттерном, но исторически фреймворк поддерживает несколько других подходов: полевое внедрение, сеттерное внедрение, XML-конфигурацию, имена бинов, квалификаторы и связанные вариации. Поэтому команда, которая хочет один предпочтительный стиль, вынуждена сама навязывать это предпочтение.

Разрыв легко визуализировать:

Что позволяет фреймворк
├── конструкторное внедрение
├── полевое внедрение
├── сеттерное внедрение
├── XML
├── сопоставление по имени бина
├── квалификаторы
└── исторические вариации

Что на самом деле хотят многие команды
└── конструкторное внедрение

Этот разрыв создаёт организационную работу. Старшие разработчики проверяют код на нежелательные паттерны, руководства по стилю объясняют, что не следует использовать, вводятся линтеры, а новые инженеры могут копировать старые примеры из интернета. Фреймворк предоставляет свободу, но команда платит за её ограничение.

Kora намеренно выбирает более узкую модель. Конструкторное внедрение — это и есть модель. Вместо зависимости от произвольной строковой идентичности используются типизированные теги. Намерение — не минимализм ради минимализма; оно в том, чтобы сделать предпочтительный паттерн обычным паттерном фреймворка, чтобы команде не приходилось многократно пересматривать одно и то же решение.

Это повторяющаяся идея в докладе:

Фреймворк может снижать когнитивную нагрузку не только добавляя возможности, но и отказываясь раскрывать выборы, которые команды на самом деле не хотят делать.

Обратная связь на этапе выполнения дороже, чем обратная связь компилятора

Затем доклад показывает ещё один, казалось бы, незначительный пример: фабричные методы, которые случайно создают коллизию из-за правил именования или композиции графа. Важная деталь — не конкретная коллизия; важно то, когда разработчик её обнаруживает.

В контейнере, ориентированном на этап выполнения, цикл обратной связи может быть таким:

написать код
   ↓
скомпилировать
   ↓
упаковать
   ↓
запустить приложение
   ↓
построить контекст
   ↓
обнаружить коллизию

Kora пытается перенести этот класс структурных ошибок на этап компиляции:

написать код
   ↓
скомпилировать граф приложения
   ↓
коллизия / отсутствующая зависимость
   ↓
ошибка компилятора

Этот сдвиг больше, чем кажется. Он означает, что фреймворк может использовать компилятор как часть модели корректности, а не полагаться на то, что разработчик запомнит, какие комбинации безопасны.

Докладчик многократно трактует это как способ снять с разработчиков знания, специфичные для фреймворка. Если граф недействителен, сборка должна падать. Если зависимость отсутствует, компилятор должен об этом сказать. Приложению не нужно запускаться, чтобы фреймворк сообщил команде, что её структура невозможна.

Жизненный цикл показывает, как быстро «простое» поведение становится тайным знанием

Жизненный цикл используется как следующий пример, потому что он выглядит простым, пока не пересекаются несколько механизмов фреймворка. Компонент может наследовать методы инициализации, использовать @PostConstruct, реализовывать интерфейсы жизненного цикла, участвовать в AOP и быть обёрнут в прокси. В этот момент даже опытный разработчик Spring может не сразу знать, какой метод инициализации выполняется первым, применяется ли аспект к конкретному колбэку или какой объект — цель или прокси — на самом деле выполняет метод.

Докладчик называет это «тайным знанием»: поведением, которое нельзя вывести из локального кода и которое вместо этого приходится помнить из документации, прошлых инцидентов или многолетнего опыта.

Ответ Kora — генерировать читаемый исходный код. Если аспект создаёт подкласс, разработчики могут открыть этот подкласс. Если релевантен метод жизненного цикла родителя, можно использовать обычную семантику Java/Kotlin, например super. Фреймворк старается сделать фактическую структуру выполнения проверяемой.

Предполагаемый рабочий процесс отладки становится таким:

Вопрос:
«Применяется ли этот аспект здесь?»

        ↓

Открыть сгенерированный класс

        ↓

Прочитать фактическую реализацию

        ↓

Узнать ответ

а не таким:

Вопрос:
«Применяется ли этот аспект здесь?»

        ↓

вспомнить правила жизненного цикла
        ↓

вспомнить правила прокси
        ↓

вспомнить правила порядка
        ↓

вспомнить поведение версии
        ↓

искать в доках / issue / статье

Различие философское: инспектируй вместо запоминания.

Сгенерированный исходный код — это исполняемое объяснение

Сгенерированный код Kora часто обсуждают в терминах производительности, но этот доклад делает равный акцент на прозрачности. Графы зависимостей, репозитории, JSON-мапперы, AOP-обёртки и HTTP-адаптеры генерируются как обычный исходный код на Java или Kotlin.

Контраст таков:

Абстракция, ориентированная на этап выполнения

исходный код
  ↓
аннотации
  ↓
внутренности фреймворка
  ↓
прокси / рефлексия / метаданные
  ↓
фактическое поведение

против:

Kora

исходный код
  ↓
аннотации
  ↓
сгенерированный исходный код
  ↓
обычный компилятор
  ↓
фактическое поведение

Сгенерированный слой становится исполняемым объяснением того, что фреймворк решил сделать.

У этого два следствия. Во-первых, в среде выполнения приложения меньше динамических механизмов. Во-вторых, фреймворку нужно меньше «фольклора», потому что разработчик может перейти к сгенерированной реализации в IDE.

Современный Kora 2 продолжает этот подход. Фреймворк существенно эволюционировал с момента этой презентации, но сгенерированный читаемый исходный код остаётся частью его архитектурной идентичности.

Конфигурация Spring: гибкость превращается в проблему приоритета

Слайды о конфигурации содержат один из самых сильных аргументов о когнитивной нагрузке во всём докладе. Spring Boot поддерживает большое количество источников свойств и определённый порядок приоритета. Слайд перечисляет 15 источников конфигурации, включая свойства по умолчанию, @PropertySource, файлы данных конфигурации, случайные значения, переменные окружения, системные свойства Java, атрибуты JNDI, параметры инициализации сервлета, SPRING_APPLICATION_JSON, аргументы командной строки, тестовые свойства, @DynamicPropertySource, @TestPropertySource и глобальные настройки, специфичные для devtools.

Вдобавок сами файлы конфигурации могут поступать из нескольких мест и объединяться в соответствии с правилами порядка.

Доклад сжимает ситуацию в такую картину:

15 возможных источников конфигурации
          ↓
4 категории / местоположения файлов конфигурации
          ↓
5 вариантов имён при ослабленном связывании
          ↓
итоговое значение

Слайд об ослабленном связывании делает проблему осязаемой. Свойство вроде hostName может быть представлено в нескольких формах:

hostName
hostname
host_name
host-name
HOST_NAME

Все они потенциально могут связываться с одной и той же целью.

Эта гибкость полезна в гетерогенных средах развёртывания. Критика докладчика не в том, что ослабленное связывание иррационально. Она в том, что разработчик должен знать о существовании всех этих источников и псевдонимов, когда отлаживает, почему значение различается между окружениями.

Ключевой вопрос становится таким:

Откуда на самом деле взялось это значение конфигурации?

Если ответ зависит от системы приоритетов, распределённой по многочисленным потенциальным источникам, фреймворк перенёс значительный объём сложности в оперативную диагностику.

Модель конфигурации Kora предпочитает типизированные интерфейсы и явные контракты, где обязательные свойства представлены напрямую, а значения по умолчанию, где возможно, выражаются через обычные конструкции языка. Цель — не «нулевая гибкость конфигурации». Это меньшая и более очевидная модель разрешения.

Сложность конфигурации — это также риск развёртывания

Доклад выходит за рамки удобства разработчика. Значение может прийти из окружения, системных свойств, аргументов командной строки, тестовой конфигурации, файлов конфигурации внутри или вне артефакта, файлов, специфичных для профиля, или других источников. Это означает, что один лишь артефакт не обязательно описывает конфигурацию, которая будет управлять производственным поведением.

Операционно полное состояние приложения ближе к:

артефакт
+ окружение
+ системные свойства
+ аргументы командной строки
+ конфигурация профиля
+ внешние файлы
+ тестовые / развёртывательные переопределения
= эффективная конфигурация

Это обычное дело в облачных приложениях и не является изначально плохим. Беспокойство докладчика в том, что количество поддерживаемых механизмов делает путь разрешения трудным для рассуждения. Функция, спроектированная для максимизации совместимости и удобства, становится ещё одной поверхностью знаний, которую разработчики должны выучить.

Тесты — это место, где сложность фреймворка умножается

Дополнительные слайды о тестировании делают эту часть аргумента гораздо сильнее. Один слайд просто говорит, по сути, «тесты Spring: мы плачем», в окружении ссылок на доклады о Spring Test, кешировании тестового контекста, антипаттернах тестирования JPA и сложности Spring Boot Test. Шутка — не важная часть. Важная часть — следующий слайд, который перечисляет вопросы, на которые разработчикам, возможно, придётся отвечать при написании модульных тестов Spring:

Переиспользовать контекст или нет?
Использовать контекст со стабами или нет?
Переопределять компоненты или нет?
Кешировать контекст или нет?
Как и когда его кешировать?
Откатывать транзакции или нет?
Предоставлять конфигурацию / профили или нет?

Рядом с этим списком — длинная колонка специализированных тестовых аннотаций: @DataJpaTest, @WebMvcTest, @JsonTest, @JdbcTest, @DataRedisTest, @SpringBootTest, @MockBean, @Transactional, @DirtiesContext, @TestPropertySource и многих других.

Экосистема тестирования Spring чрезвычайно способна, но способность и простота — не одно и то же. Тестовый фреймворк может стать собственной моделью программирования.

Когнитивный путь может выглядеть так:

Что я хочу протестировать?
      ↓
Какой тестовый срез?
      ↓
Какой контекст?
      ↓
Какие моки?
      ↓
Будет ли контекст кеширован?
      ↓
Нужен ли мне @DirtiesContext?
      ↓
Что происходит с транзакциями?
      ↓
Какие свойства активны?

Критика докладчика в том, что это много размышлений о фреймворке до того, как написано хоть одно утверждение о приложении.

Модель тестирования Kora нацелена на более простое отношение: тестовый граф выводится из компонентов, явно нужных тесту, и их зависимостей. Замены, стабы и конфигурация явны. В упрощённой форме:

Что объявляет тест
        =
что содержит тестовый граф

Фреймворк может запускать небольшой граф достаточно быстро, чтобы агрессивное кеширование контекста было менее необходимо как оптимизация по умолчанию. Если переиспользование желательно, его можно запросить намеренно.

Это важно, потому что тестирование выполняется непрерывно. Правило фреймворка, на изучение которого уходит одна минута один раз, дёшево. Модель тестирования, требующая повторных рассуждений в тысячах тестов, становится дорогой.

Кеширование контекста — симптом стоимости запуска

Аргумент доклада о тестировании напрямую связан с производительностью запуска. Сложное кеширование тестового контекста Spring существует не просто так: запуск контекста приложения может быть дорогим. Поэтому кеширование — это оптимизация, которая делает большие тестовые наборы практичными.

Но кеширование вносит собственную сложность:

медленный запуск контекста
      ↓
кешировать контекст
      ↓
определить правила переиспользования
      ↓
инвалидировать выборочно
      ↓
понять утечку состояния
      ↓
выучить больше правил фреймворка

Kora пытается атаковать проблему раньше в цепочке. Если построение графа дёшево, каждый тест может чаще получать свежий граф, не заставляя разработчиков оптимизировать переиспользование контекста.

Ключевая идея архитектурна:

Хорошая оптимизация иногда — не лучший кеш, а удешевление некешируемой операции.

Раздел об аспектах: корректность прежде производительности

Раздел об аспектах в докладе начинается с примера производственного стиля, а не бенчмарка. У метода сервиса есть несколько сквозных аннотаций — например, валидация, кеширование, повторы, метрики, логирование или другие политики. У разработчика два основных вопроса:

Все ли они на самом деле выполняются?
В каком порядке?

Доклад показывает, почему эти вопросы могут быть нетривиальными. Разные модули Spring не всегда поддерживают одинаковые сигнатуры методов или типы возвращаемых значений. Аннотация кеширования может существовать в коде, пока необходимый модуль отсутствует. Выражение внутри аннотации может упасть только когда аспект реально выполняется. Бизнес-тест может пройти, даже если поведение кеша никогда не запускалось.

Это источник одной из самых запоминающихся строк доклада:

Если это работает, это не обязательно означает, что каждая функция фреймворка вокруг этого работает.

Вторая проблема — порядок. Порядок аспектов может зависеть от приоритета, определённого реализацией, общих значений по умолчанию или глобальной конфигурации. Порядок, видимый в исходных аннотациях, может не быть порядком, который фактически выполняется.

Аспекты Spring: резюме докладчика

Один из новых слайдов даёт краткое резюме аргумента докладчика об аспектах Spring:

  1. «стеклянный потолок» фиксированных накладных расходов;
  2. негативный совокупный эффект;
  3. неочевидный порядок вызовов;
  4. ограниченная способность гибко менять порядок;
  5. неприятные цепочки вызовов и отладка.

Слайд также признаёт одно преимущество:

писать собственные аспекты в Spring проще.

Эта уступка важна. Доклад не утверждает, что Kora выигрывает каждое сравнение эргономики. Системы прокси времени выполнения могут делать настраиваемое перехватывание чрезвычайно гибким, потому что могут применять поведение динамически, без генерации на этапе компиляции.

Поэтому обмен более сбалансирован:

AOP Spring
+ лёгкая динамическая настройка
+ огромная экосистема
- накладные расходы прокси времени выполнения
- порядок может быть косвенным
- больше кадров вызовов фреймворка

AOP Kora
+ сгенерированные прямые вызовы
+ явный порядок
+ читаемый исходный код
+ низкие накладные расходы времени выполнения
- модель этапа компиляции
- менее динамичен

Это более полезное сравнение, чем простое объявление одной модели универсально превосходящей.

Измерение фиксированной стоимости аспектов

Затем презентация бенчмаркает сам механизм аспектов. Сравниваются три варианта:

Базовый Java
    рукописные вызовы до/после

Kora
    сгенерированная на этапе компиляции обёртка аспекта

Spring
    путь прокси / перехватчика времени выполнения

Первый микробенчмарк намеренно даёт аспектам почти никакой полезной работы. Смысл — выявить накладные расходы фреймворка.

Слайд сообщает:

Случай Java Kora Spring
1 аспект 1,526,304,670 ops/s 1,305,622,052 ops/s 1,589,510 ops/s
5 аспектов 1,533,758,344 ops/s 1,305,990,807 ops/s 314,724 ops/s

Заголовок слайда гласит, что Kora примерно в 4 149× быстрее Spring для пяти пустых аспектов.

Простое представление:

5 пустых аспектов
операций / секунду

Java    ~1.534B  ██████████████████████████████
Kora    ~1.306B  █████████████████████████
Spring  ~0.315M  ▏

Это не означает, что сервис на Kora в 4 149 раз быстрее сервиса на Spring. Бенчмарк намеренно убирает полезную работу, чтобы результатом стали фиксированные механизмы. Число описывает стоимость этого конкретного механизма в искусственном микробенчмарке.

Это ограничение — не слабость измерения; это цель измерения. Микробенчмарки изолируют конкретный механизм.

Полезный вывод таков:

Путь прокси Spring имеет измеримые фиксированные накладные расходы.

Сгенерированные прямые вызовы Kora гораздо ближе
к рукописному Java, когда перехватываемая работа крошечная.

Имеет ли это значение в производственной системе, зависит от того, как часто происходят такие вызовы и сколько полезной работы их окружает.

Почему пустые аспекты не полностью искусственны

Докладчик отвечает на очевидное возражение: производственные аспекты обычно что-то делают. Однако аспект логирования может просто проверить уровень логирования и вернуться. Некоторые пути метрик, кеша или валидации также могут выполнять очень мало работы в определённых случаях.

Когда полезная работа мала, фиксированная стоимость прокси становится большим процентом от всей операции.

Затем доклад увеличивает объём работы внутри аспекта. Как и ожидалось, относительная разница уменьшается. Как только каждый аспект выполняет значительные вычисления, накладные расходы фреймворка разбавляются бизнес-работой.

Связь такова:

почти нет полезной работы
      → доминирует инфраструктура

умеренная полезная работа
      → накладные расходы всё ещё видны

большая полезная работа
      → стоимость фреймворка — меньший процент

Производственный аргумент докладчика в том, что методы сервисов часто вложены и многократно перехватываются, поэтому небольшие затраты могут накапливаться по цепочкам вызовов.

Стек-трейсы и отладка — часть той же стоимости

Производительность — только половина аргумента об AOP. Цепочки прокси/перехватчиков времени выполнения также появляются в стек-трейсах и отладчиках.

Вызов Spring может выглядеть концептуально так:

бизнес-метод
      ↑
прокси
      ↑
перехватчик
      ↑
диспетчер AOP
      ↑
вызов фреймворка
      ↑
...

Сгенерированный путь Kora может выглядеть больше так:

сгенерированная обёртка
      ↓
бизнес-метод

Точный стек зависит от интеграции, но принцип прост: меньше слоёв абстракции времени выполнения дают более короткие и более ориентированные на приложение трейсы.

Поэтому один и тот же проектный выбор одновременно влияет на накладные расходы CPU и на отлаживаемость.

У оптимизации своя цена

Новые слайды о «цене оптимизации» делают следующий раздел гораздо яснее. Резюме докладчика таково:

  1. разработчики тратят много времени на оптимизации фреймворка;
  2. ленивая инициализация — не обязательно реальная оптимизация;
  3. оптимизация может требовать жертвы функциональностью;
  4. крупные оптимизации часто слишком дороги для широкого внедрения командами;
  5. выгода может не оправдывать уплаченную цену.

Это и есть настоящий предмет раздела об оптимизации. Дело не в том, можно ли сделать Spring быстрее. Конечно, можно. Вопрос в том, сколько дополнительной экспертизы и работы по сопровождению требуется, чтобы достичь этого состояния.

Ленивая инициализация перемещает работу, а не устраняет её

Ленивая инициализация — любимый пример докладчика. Она может улучшить заявленное время запуска, откладывая создание бина до первого использования бина.

Возражение операционное:

процесс запускается быстро
      ↓
readiness говорит «готов»
      ↓
приходит производственный трафик
      ↓
начинается ленивая инициализация
      ↓
первые запросы платят стоимость запуска

Если Kubernetes уже заменил старый под новым, приложение может испытывать задержку ровно тогда, когда должно принимать трафик.

Это не означает, что ленивая инициализация всегда неверна. Более сильная мысль в том, что перенос работы за пределы измеряемого интервала запуска не следует автоматически описывать как устранение работы.

Оптимизация авто-конфигурации может стать ручным графом зависимостей

Другой слайд показывает визуальную «цену» оптимизации: большой явный список классов конфигурации и правил порядка. Сообщение намеренно драматично, но оно иллюстрирует реальный обмен.

Spring Boot начинается с авто-конфигурации, управляемой classpath, потому что это удобно. Если запуск или занимаемая площадь становятся проблемой, команды могут начать исключать или явно импортировать только выбранные авто-конфигурации. В этот момент им приходится понимать внутренний граф конфигурации, управлять которым автоматически они изначально платили фреймворку.

Жизненный цикл становится таким:

Автоматическое обнаружение
      ↓
инициализируется слишком много
      ↓
оптимизировать запуск
      ↓
вручную выбрать конфигурации
      ↓
понять порядок и зависимости
      ↓
поддерживать слой оптимизации

Оптимизация может стать второй архитектурой приложения.

Kora вместо этого начинает с явной композиции модулей. Если модуль не подключён к графу приложения, он не является частью приложения. Фреймворк платит за эту явность заранее, а не требует более поздней фазы, цель которой — отменить автоматическое поведение.

Бенчмарк оптимизированного запуска

Докладчик строит более реалистичный сервис на Spring с несколькими контроллерами, слоями сервисов, метриками, health-пробами, OpenAPI, аспектами и интеграциями. Затем он собирает набор техник оптимизации Spring из статей и докладов с конференций и применяет подмножество, которое считает практичным.

Окружение бенчмарка, показанное на слайдах, использует Docker-контейнер с 1 CPU и 1 ГБ памяти, работающий на MacBook Pro 2019 года с Intel i7-9750H. Цифры усреднены по пяти запускам.

Заявленные времена запуска:

Вариант Время запуска
Kora 4.89 с
Оптимизированный Spring 21.13 с
Spring 26.53 с

Слайд резюмирует это как запуск Kora примерно в 5.4× быстрее, чем обычный Spring, и в 4.3× быстрее, чем оптимизированная версия Spring, в этом бенчмарке.

Запуск в Docker
1 CPU / 1 ГБ
меньше — лучше

Kora              █████                         4.89 с
Spring оптимизир. █████████████████████        21.13 с
Spring            ███████████████████████████  26.53 с

Эти соотношения зависят от рабочей нагрузки и окружения. Их не следует читать как универсальные законы запуска Spring/Kora. Их роль в презентации — показать, что большой объём работы по оптимизации не стёр архитектурное различие в этом конкретном сервисе.

Оптимизация может перенести стоимость на этап сборки

Kora выполняет значительную обработку на этапе компиляции, поэтому очевидный следующий вопрос — не означает ли более быстрый этап выполнения просто более медленные сборки.

Бенчмарк чистой сборки без кешей, демонов и других ускорителей сообщает:

Вариант Чистая сборка
Spring 9.33 с
Kora 11.63 с
Оптимизированный Spring 18.69 с
Чистая сборка
меньше — лучше

Spring            █████████                 9.33 с
Kora              ████████████             11.63 с
Spring оптимизир. ███████████████████      18.69 с

Этот результат важен, потому что ясно показывает перенос стоимости. Kora не бесплатен. Обработка аннотаций и генерация кода потребляют время сборки.

Оптимизированное приложение на Spring становится медленнее собираться, потому что часть его оптимизационных механизмов переносит дополнительную работу в этапы упаковки и сборки.

Второй слайд показывает более обычную сборочную разработку с включёнными кешами, демонами, инкрементальной работой и связанными оптимизациями:

Вариант Сборка для разработки
Spring 1.89 с
Kora 2.51 с
Оптимизированный Spring 2.84 с
Обычная сборочная разработка
меньше — лучше

Spring            ███████████████████       1.89 с
Kora              █████████████████████████ 2.51 с
Spring оптимизир. ████████████████████████████ 2.84 с

Это одно из самых сбалансированных измерений в докладе. Kora платит больше во время компиляции, но разница в обычных условиях сборки разработчика не огромна в представленном проекте.

Фреймворки с этапом компиляции не устраняют работу — они её перемещают

Измерения сборки/запуска делают архитектурный обмен явным:

Подход с тяжёлым этапом выполнения

меньше работы фреймворка во время компиляции
        ↓
больше обнаружения / прокси / работы при запуске на этапе выполнения

против:

Подход с этапом компиляции

больше анализа и генерации во время сборки
        ↓
меньше обнаружения и динамической работы фреймворка на этапе выполнения

Ни одна архитектура не отменяет сложность.

Вопросы таковы:

  • где происходит работа;
  • когда ошибки становятся видимыми;
  • как часто работа повторяется;
  • можно ли проверить результирующую реализацию;
  • какие операционные затраты остаются после развёртывания?

Ставка Kora в том, что время сборки — лучшее место для механизмов фреймворка, потому что компиляцией управляют разработчики и CI, тогда как работа на этапе выполнения оплачивается при каждом запуске и, возможно, при каждом запросе.

Слайд «Конфигурация Spring» — идеальный пример когнитивного накопления

Слайд о конфигурации особенно полезен, потому что ни один отдельный пункт на нём не выглядит неразумным. Переменные окружения полезны. Системные свойства полезны. Аргументы CLI полезны. Тестовые источники свойств полезны. Файлы, специфичные для профиля, полезны. Ослабленное связывание полезно.

Проблема появляется только когда они все объединены.

Это совокупный паттерн, который критикует доклад:

функция A полезна
функция B полезна
функция C полезна
...
функция N полезна
        ↓
все сосуществуют
        ↓
разработчик должен понимать взаимодействия

Зрелый фреймворк становится трудным не потому, что какая-то одна функция абсурдна, а потому, что совместимость одновременно поддерживает старые и новые подходы.

Это одна из областей, где Kora имеет врождённое преимущество как более молодой, более узкий фреймворк. Он может отказаться от некоторых слоёв совместимости, которые Spring не может убрать, не ломая огромные экосистемы.

Итоговое резюме Spring: широта имеет последствия

Один из завершающих слайдов резюмирует критику Spring докладчиком в семи пунктах:

  1. архаичные инструменты и архитектурный багаж;
  2. множество способов выполнить одну и ту же задачу;
  3. переусложнённые инструменты и тестирование;
  4. разработчики должны держать в голове большой объём контекста;
  5. время тратится на оптимизацию, обходные пути и «магию»;
  6. обратная связь не всегда очевидна;
  7. рекомендации и лучшие практики не всегда достаточно явны со стороны самого фреймворка.

Слайд использует гораздо более резкие формулировки, чем должна нейтральная инженерная статья, но обмен реален. Широкая историческая совместимость Spring создаёт огромную поверхность возможностей. Эта поверхность порождает больше выборов, больше взаимодействий и больше знаний, которыми команды должны управлять.

Важное сравнение — не такое:

Spring плох
Kora хорош

Оно такое:

Spring
широкая историческая совместимость
огромная экосистема
много моделей и точек расширения
динамическая гибкость
        ↓
большая поведенческая поверхность

против:

Kora
более узкая модель
композиция на этапе компиляции
сгенерированный исходный код
явные модули
меньше параллельных подходов
        ↓
меньшая поведенческая поверхность

Разные организации могут предпочесть разные обмены.

Цель Kora — сделать оптимизацию архитектурой по умолчанию

Сильнейшая часть аргумента о Kora не в том, что у фреймворка есть секретный переключатель запуска или одна необычно быстрая реализация сервера. Смысл в том, что многие из «оптимизаций», которые обсуждает докладчик, — просто следствия обычной архитектуры Kora.

Граф компилируется.

Компоненты явны.

AOP генерируется.

Сериализация генерируется.

Репозитории генерируются.

Модули подключаются по выбору.

Меньше обнаружения на этапе выполнения, которое нужно отключать позже.

Это меняет того, кто отвечает за оптимизацию:

Путь в стиле Spring в докладе

удобство по умолчанию
      ↓
проблема производительности
      ↓
экспертная оптимизация
      ↓
специальная конфигурация
      ↓
бремя сопровождения

против:

Kora

модель этапа компиляции по умолчанию
      ↓
этап выполнения уже лёгкий
      ↓
разработчик оптимизирует бизнес-логику

Это гораздо более сильный аргумент, чем любой отдельный бенчмарк.

Kora не представлен как универсальная замена

Несмотря на агрессивный стиль доклада, вывод более сдержан, чем средние разделы. Докладчик явно говорит, что если Spring решает проблемы команды и команда довольна, это допустимый выбор.

Намерение — не установить новую догму.

Оно в том, чтобы оспорить допущение, что сама Spring и есть догма.

Фреймворк — это технология. У неё есть сильные стороны, компромиссы, исторические ограничения и операционные затраты. Команды должны быть готовы оценивать альтернативы, когда эти затраты становятся значительными.

Это в равной степени относится к Kora. Более узкая модель Kora полезна именно потому, что он не пытается быть всем для всех.

AspectJ — важная оговорка

В Q&A кто-то спрашивает, изменил бы бенчмарк аспектов compile-time weaving в AspectJ. Докладчик признаёт, что мог бы. Вплетение на этапе компиляции — это другой механизм, чем тестируемый обычный путь прокси/перехватчика времени выполнения.

Это различие важно. Бенчмарк — не «каждый возможный способ реализации AOP в экосистеме Spring против Kora». Это тестируемый путь AOP Spring, используемый релевантными модулями, против сгенерированного исходного кода и рукописного Java в Kora.

Эта оговорка улучшает техническую интерпретацию результатов.

Отсутствие специального IDE-плагина — часть дизайна

В Q&A также спрашивают о поддержке IDE. Ответ в том, что Kora намеренно старается не требовать специфичного для фреймворка плагина для базового понимания.

Если зависимости представлены как типы, работает обычная навигация.

Если код генерируется как исходный код, IDE может его открыть.

Если граф приложения компилируется, работает диагностика компилятора.

Инструментарий остаётся в основном обычным инструментарием Java/Kotlin.

Это ещё одно выражение той же философии:

Предпочитайте семантику языка и компилятора параллельному семантическому слою фреймворка.

Kotlin использует ту же архитектурную модель

Докладчик подтверждает поддержку Kotlin как первоклассной части Kora. Фреймворк не спроектирован вокруг контейнера времени выполнения только для Java, где Kotlin рассматривается лишь как альтернативный синтаксис.

Генерация на этапе компиляции означает, что качество интеграции Kotlin очень важно, потому что обнуляемость, аргументы по умолчанию и информация о типах напрямую влияют на сгенерированный код.

Текущее поколение Kora 2 продолжает развивать эту симметрию Java/Kotlin.

Исторический контекст: это Kora 1.x, а не Kora 2

Версия, обсуждаемая в презентации, относится к поколению Kora 1.x. Современный Kora 2 продвинул несколько из этих идей дальше, а не отказался от них.

Упрощённая эволюция такова:

Ранний Kora
    ↓
доказать DI на этапе компиляции
и архитектуру сгенерированного исходного кода

Kora 1.x
    ↓
расширить модули
созреть AOP / тестирование / телеметрию / OpenAPI
расти в производственном внедрении

Kora 2.x
    ↓
снова упростить
синхронные контракты приложений
исполнение с приоритетом виртуальных потоков
убрать реактивные / suspend API фреймворка
уточнить обработку графа
расширить доки и производственные рекомендации

Поэтому поколение 2.0 — ещё более сильное выражение тезиса доклада «сократите число моделей». Вместо бесконечного накопления стилей исполнения Kora убрал несколько контрактов фреймворка и стандартизировался вокруг более простой синхронной модели.

Как правильно интерпретировать бенчмарки

Доклад содержит намеренно драматичные числа, особенно вокруг AOP. Лучший способ их интерпретировать — различать три уровня:

Микробенчмарк
→ изолирует накладные расходы механизма

Бенчмарк приложения
→ механизм + работа приложения

Производственная система
→ приложение + I/O + БД + сеть
   + очереди + масштабирование + операционное поведение

Разрыв в 4 149× в микробенчмарке с пустыми аспектами не означает преимущества в 4 149× в производственной пропускной способности.

Но и отбрасывать число как синтетическое было бы ошибкой. Оно демонстрирует, что сам механизм имеет значительную фиксированную стоимость на тестируемом пути.

Точно так же запуск Kora за 4.89 секунды против запуска Spring за 26.53 секунды в одном Docker-бенчмарке не означает, что каждый сервис на Kora запускается в 5.4 раза быстрее каждого сервиса на Spring. Это демонстрирует, как две архитектуры вели себя в этом конкретном сервисе, окружении и конфигурации.

Научно полезное чтение таково:

Бенчмарк определяет, где существуют накладные расходы. Производственная значимость зависит от частоты рабочей нагрузки и масштаба.

Экономика фреймворка: небольшие затраты, умноженные на организацию

Самый глубокий аргумент в презентации организационный, а не микроархитектурный.

Двадцать минут на изучение оптимизации — пустяк.

Двадцать минут, умноженные на сотни разработчиков и множество сервисов, — не пустяк.

Несколько микросекунд накладных расходов прокси — пустяк.

Те же накладные расходы на миллиардах вызовов могут не быть пустяком.

Одна запутанная тестовая аннотация — пустяк.

Целая организация, поддерживающая тысячи тестов вокруг сложных правил контекста, — не пустяк.

Докладчик фактически предлагает такую модель:

небольшая стоимость фреймворка
        ×
разработчики
        ×
сервисы
        ×
тесты
        ×
сборки
        ×
развёртывания
        ×
запросы
        ×
годы
        =
стоимость масштаба платформы

В этом экономический смысл метафоры «Докторской по Spring».

Цена — накопленное инженерное внимание.

Заключение: настоящая проблема — постоянный обмен между возможностями и последствиями

Сильнейший завершающий слайд в докладе — не бенчмарк. Это резюме, озаглавленное без обиняков «Spring bullshit», за которым следуют карточка бинго с концепциями фреймворка и предложение, улавливающее весь аргумент докладчика: Spring становится бесконечным балансированием между функциональностью и последствиями этой функциональности.

Эта рамка важна, потому что презентация на самом деле не об одной плохой аннотации, одном медленном прокси, одном запутанном тестовом срезе или одном дорогом пути запуска. Критика докладчика совокупна. Сильные стороны Spring неотделимы от его истории: огромная экосистема, долгосрочная совместимость, широкая авто-конфигурация, несколько моделей программирования, обширная тестовая инфраструктура и огромное число интеграций. Каждая из этих сильных сторон существует не просто так, но каждая также расширяет поведенческую поверхность, которую разработчики должны понимать.

Финальный слайд сжимает эту накопленную стоимость в семь жалоб:

  1. Архаичные инструменты и архитектурный багаж. Старые решения остаются видимыми, потому что совместимость важна, поэтому современные приложения наследуют концепции, возникшие при совершенно иных допущениях о JVM и развёртывании.
  2. Множество способов выполнить одну и ту же задачу. Гибкость становится проблемой управления, когда команды должны решать, какие поддерживаемые механизмы всё ещё приемлемы, а какие лишь исторические.
  3. Переусложнённые инструменты и тестирование. Фреймворк не просто помогает тестировать приложение; командам может понадобиться существенная экспертиза в самом тестовом фреймворке.
  4. Большой объём контекста должен держаться в головах разработчиков. Жизненный цикл, прокси, приоритет свойств, тестовые контексты, порядок аспектов, авто-конфигурация, профили и правила оптимизации становятся параллельным корпусом знаний.
  5. Время теряется на оптимизацию, обходные пути и «магию». Когда обычный путь фреймворка становится дорогим, разработчики начинают тратить инженерное время на изучение того, как частично отменить или ограничить его.
  6. Обратная связь не всегда очевидна или немедленна. Некоторые структурные проблемы появляются только когда запускается контекст, выполняется конкретный аспект или активируется конкретная комбинация конфигурации.
  7. Рекомендуемые практики не всегда очевидны со стороны самого фреймворка. Команды часто зависят от докладов с конференций, статей, обсуждений на Stack Overflow, институциональных знаний и старших разработчиков, чтобы определить предпочтительный путь.

По отдельности ни один из этих пунктов не доказывает, что Spring — плохой фреймворк. Вместе они описывают стоимость фреймворка, который более двух десятилетий оптимизировался под широту, совместимость и гибкость.

Поэтому метафора «бинго» — больше, чем шутка. Она иллюстрирует, как сложность фреймворка накапливается по горизонтали. Команда начинает с внедрения зависимостей, затем добавляет транзакции, кеширование, безопасность, конфигурацию, тестовые срезы, метрики, авто-конфигурацию, колбэки жизненного цикла, языки выражений, пользовательские квалификаторы, правила оптимизации и всё более специализированные аннотации. Каждый квадрат полезен. Проблема появляется, когда активно достаточно квадратов, чтобы разработчики тратили больше времени на рассуждения об их взаимодействиях, чем о бизнес-операции, которую фреймворк должен был упростить.

Упрощённая версия аргумента слайда выглядит так:

Больше возможностей фреймворка
        ↓
больше конвенций
        ↓
больше взаимодействий
        ↓
больше граничных случаев
        ↓
больше экспертных знаний
        ↓
больше оптимизаций / обходных путей
        ↓
больше когнитивной нагрузки, специфичной для фреймворка

Это цикл, который докладчик называет «вечным бинго»: команды непрерывно балансируют новую функциональность против вторичной сложности, которую она вносит.

Затем разделы бенчмарков в докладе дают этому философскому аргументу нечто измеримое. Конфигурация Spring может включать 15 источников свойств, несколько местоположений файлов конфигурации и множество вариантов ослабленного связывания. Тесты Spring могут требовать решений о переиспользовании контекста, стабах, переопределениях, кешировании, откате транзакций, активной конфигурации и большой коллекции специализированных аннотаций. Композиция аспектов может быть корректной, но неочевидной, а механизм прокси времени выполнения имеет измеримые фиксированные накладные расходы. Запуск можно улучшить, но сама оптимизация может ввести дополнительные шаги сборки, выбор конфигурации, знания о порядке и работу по сопровождению.

Kora выбирает другой обмен. Он компилирует граф приложения. Он генерирует обёртки аспектов. Он генерирует JSON-ридеры и райдеры. Он генерирует репозитории. Он сохраняет SQL видимым. Он использует типизированную конфигурацию. Он делает модули явными. Он принимает дополнительную работу на этапе компиляции в обмен на меньшее количество механизмов времени выполнения и меньший объём скрытого состояния фреймворка.

Измерения в докладе иллюстрируют этот обмен, а не доказывают универсального победителя. В микробенчмарке с пустыми аспектами рукописный Java и Kora работают в масштабе примерно миллиарда операций в секунду, тогда как тестируемый путь AOP Spring падает до масштаба миллионов или суб-миллионов. В Docker-бенчмарке запуска Kora стартует за 4.89 секунды по сравнению с 21.13 секунды для оптимизированной конфигурации Spring и 26.53 секунды для конфигурации Spring по умолчанию. При чистой сборке Kora медленнее обычного Spring, потому что генерация кода делает реальную работу: * 11.63 секунды против 9.33 секунды. При обычных сборочных разработках этот разрыв сужается до *2.51 секунды против 1.89 секунды**.

Этот баланс и есть настоящая история. Kora не устраняет работу фреймворка; он намеренно выбирает другое место, чтобы за неё платить.

Архитектура в стиле Spring в докладе

гибкость времени выполнения
динамическое обнаружение
прокси
большая поверхность совместимости
много пересекающихся механизмов
        ↓
больше работы на этапе выполнения
больше знаний, специфичных для фреймворка
больше работы по оптимизации позже

против:

Архитектура в стиле Kora

анализ на этапе компиляции
сгенерированный исходный код
явный граф
более узкая модель
меньше параллельных механизмов
        ↓
больше работы на этапе сборки
меньше механизмов времени выполнения
меньше скрытого контекста

Поэтому вывод докладчика — не просто «Spring медленный», и даже не «Kora быстрее». Более сильное утверждение в том, что у сложности фреймворка есть сложный процент. Каждый удобный механизм, который добавляет ещё одно правило, ещё одно ограничение порядка, ещё одну тестовую аннотацию или ещё одну технику оптимизации, может быть локально разумным, но при этом увеличивать общую стоимость понимания системы.

Именно поэтому финальный слайд так важен. Он переформулирует проблему с отдельных функций на архитектуру. Проблема Spring, по мнению докладчика, не в каком-то одном квадрате на карточке бинго. Она в том, что после достаточного числа лет и функций фреймворк заставляет команды вступать в непрерывные переговоры между возможностями и последствиями.

Дизайн Kora пытается разорвать этот цикл, делая доступными меньше выборов, перенося больше валидации в компиляцию, генерируя проверяемый исходный код и делая обычный путь уже похожим на оптимизированный. Цель — не устранить экспертизу, а перенаправить её с мелочей фреймворка на Java, Kotlin, SQL, HTTP, конкурентность, распределённые системы, базы данных, наблюдаемость и само приложение.

Какая модель лучше, всё ещё зависит от приложения и организации. Широта экосистемы и совместимость Spring могут быть чрезвычайно ценными. Более узкая модель Kora может быть ценной именно тогда, когда команды решают, что накопленная стоимость гибкости стала слишком высокой.

Провокационная фраза в докладе — «Докторская по Spring».

Более точный инженерный перевод таков:

Фреймворк не должен делать свою собственную сложность тем, в чём инженеры становятся экспертами. Его функциональность должна оправдывать когнитивные, операционные и связанные с сопровождением последствия, которые с ней приходят.

Или, на языке завершающего слайда:

Настоящая проблема фреймворка начинается, когда добавление функциональности снова и снова создаёт ещё один квадрат на карточке бинго, который разработчики должны помнить вечно.