От переводчика: поскольку Spring Framework является одним из основных фреймворков, на которых мы строим CUBA, то новости о новых возможностях Spring не проходят незаметно для нас. Ленивая инициализация — один из способов уменьшить время первой загрузки приложения, что в наш век повсеместного использования микросервисов является важной метрикой. Для тех, кто предпочитает чтению просмотр видео, есть 10-ти минутное выступление Josh Long, посвященное теме статьи.
Недавно анонсированный первый milestone релиз Spring Boot 2.2 добавляет поддержку ленивой инициализации. В этой статье мы рассмотрим новую функциональность и объясним, как ее включить.
Что это значит — быть ленивым?
Spring Framework поддерживает ленивую инициализацию с тех пор, как его исходный код переехал в git одиннадцать лет назад. По умолчанию, когда контекст приложения обновляется, каждый бин создается заново и производится внедрение его зависимостей. В отличие от этого, если бин сконфигурирован для ленивой инициализации, он не будет создан и его зависимости не будут проставлены, пока в этом нет необходимости.
Включение ленивой инициализации
В любой версии Spring Boot есть возможность включения ленивой инициализации, если вы не против замарать руки работой с BeanFactoryPostProcessor
. Spring Boot 2.2 просто упрощает этот процесс, вводя новое свойство — spring.main.lazy-initialization
(есть также эквивалентные методы в SpringApplication
и SpringApplicationBuilder
). Когда это свойство установлено в true
, бины приложения будут сконфигурированы так, чтобы использовалась ленивая инициализация.
Преимущества ленивой инициализации
Ленивая инициализация может заметно уменьшить время старта вашего приложения, поскольку на этом этапе загружается меньше классов и создается меньше бинов. Например, маленькое веб-приложение которое использует Actuator и Spring Security, обычно стартует 2,5 секунды. А с ленивой инициализацией этот процесс занимает 2 секунды. Точные величины ускорения будут меняться от приложения к приложению, в зависимости от структуры графа зависимостей бинов.
Примечание переводчика: я запускал вот этот пример, прописав в зависимостях Spring Boot 2.2, и время запуска с ленивой инициализацией было 3 секунды, а без нее — 4. Думаю, что на более серьезных приложениях, существенного выигрыша во времени старта за счет использования ленивой инициализации мы не увидим.
А что там насчет DevTools?
Spring Boot DevTools предоставляют заметное ускорение разработки. Вместо перезапуска JVM и приложения каждый раз, когда вы что-то меняете, DevTools делают “горячий перезапуск” приложения в той же самой JVM. Значительное преимущество такого перезапуска в том, что он дает JIT возможность оптимизировать код, который исполняется при старте приложения. После нескольких перезапусков, исходное время в 2,5 секунды уменьшается почти на 80% до 500 мс. С ленивой инициализацией все обстоит ещё лучше. Установка свойства spring.main.lazy-initialization
показывает время перезапуска непосредственно в IDE равное 400 мс.
Обратная сторона ленивой инициализации
Как было показано выше, включение ленивой инициализации может достаточно серьезно уменьшить время запуска приложения. И, возможно, у вас будет непреодолимое желание использовать это постоянно или, как минимум, вы будете задаваться вопросом, почему ленивая инициализация не включена по умолчанию. Есть несколько возможных негативных эффектов, которые лучше прояснить сразу.
Тот факт, что классы не загружаются, а бины не создаются до того момента, пока они не потребуются, может маскировать проблемы, которые раньше могли бы быть выявлены уже на этапе запуска приложения. Например, это может быть отсутствие нужного класса, переполнение памяти или ошибка, связанная с неправильной конфигурацией.
В веб-приложениях ленивая конфигурация может увеличить латентность HTTP запросов, которые вызывают инициализацию бинов. Обычно это первый запрос, но могут быть дополнительные нежелательные эффекты, затрагивающие балансировку нагрузки или автоматическое масштабирование.
Эта штука включена?
Если вы не уверены, как именно ленивая инициализация влияет на ваше приложение или вы хотите проверить, что остальные аспекты фреймворка вам подходят и делают то, что нужно, то вам будет полезно использовать отладчик для этого. Установив точку прерывания на конструкторе бина, можно посмотреть, в какой именно момент бин инициализируется. Например, в веб-приложении, написанном на Spring Boot и с включенной ленивой инициализацией, можно видеть, что бины, помеченные аннотацией @Controller
, не создаются до первого запроса к DispatcerServlet
Spring MVC или к DispatchHandler
Spring WebFlux.
Когда включать ленивую инициализацию?
Как мы уже видели выше, ленивая инициализация предлагает заметные улучшения во время запуска приложения, но также есть и обратные стороны, так что нужно очень аккуратно использовать эту возможность.
Одна область, где ленивая инициализация может принести дивиденды (почти без накладных расходов) — это процесс разработки приложения. Пока вы пишете приложение, уменьшенное время перезапуска, которое обеспечивается ленивой инициализацией в комбинации с DevTools, может значительно сэкономить вам время.
Где ещё можно получить преимущества от использования ленивой инициализации — так это в интеграционных тестах. Вы, возможно, уже используете "нарезку" тестов для уменьшения времени выполнения, ограничивая количество инициализирующихся бинов в некоторых типах тестов. Ленивая же инициализация предоставляет альтернативную возможность для достижения такого же результата. Если вы не в той должности, чтобы менять структуру приложения для “нарезать” тестов, или для конкретно ваших тестов нет подходящей “нарезки”, то включение ленивой инициализации ограничит количество бинов теми, которые используются только в вашем тесте. Это уменьшит время выполнения теста, особенно если они запускаются в изолированной среде во время разработки.
Включайте ленивую инициализацию на проде в последнюю очередь. И, если вы решили это сделать, делайте это с осторожностью. Для веб-приложений менеджер контейнеров может полагаться на точку входа /health
, которая обычно отвечает довольно быстро, но надо помнить, что, потенциально, первые вызовы могут выполняться дольше обычного. Нужно также помнить про размер памяти, выделяемый для JVM, чтобы не столкнуться с переполнением, когда все компоненты будут инициализированы.
Автор: a_belyaev