Введение
Ни для кого не секрет, что Spring Framework один из самых популярных фреймворков для приложений на языке Java. Он интегрировал в себя самые полезные и актуальные технологии, такие как i18n, JPA, MVC, JMS, Cloud и т.п.
Но насколько хорошо вы знакомы с жизненным циклом фреймворка? Наверняка вы сталкивались с проблемами поднятия контекста и освобождением ресурсов при его остановке, когда проект разрастается. Сегодня я попытаюсь наглядно показать вам это.
Я решил изучить эту тему так как официальная документация достаточно разбросано отвечает на этот вопрос. В итоге я разработал небольшой тестовый стенд. Для тестирования я использовал специальные интерфейсы интеграции и аннотации которые позволяют расширить контейнер Spring-а, которые позволяют внедрить кастомную логику в его жизненный цикл. Я выбрал эти инструменты, так как там обычно и возникают проблемы. Я считаю, что это необходимо знать, чтобы избежать проблем на стадии разработки, отладки и тестирования.
К слову в этой статье не обсуждается почему жизненный цикл такой, какой он есть. Здесь я хочу вам показать результаты моих исследований и выводы. Также здесь не рассматриваются нюансы жизненного цикла иерархических контекстов.
Краткий экскурс
В данном подразделе я кратко расскажу об инструментах интеграции, которые предоставляет Spring. Вы можете пропустить его если вы знакомы со следующими понятиями: IoC Container, DI, BeanDefinition
, BeanFactory
, ApplicationContext
, BeanFactoryPostProcessor
, BeanPostProcessor
, ApplicationListener
, Lifecycle
, SmartLifcycle
.
Инверсия управления (Inversion of Control) - это принцип, при котором фреймворк вызывает пользовательский код. Это отличается от случая с библиотеками, потому что пользовательский код вызывает код библиотеки.
Внедрение зависимостей (Dependency Injection) - это шаблон проектирования, в котором объект получает объекты, от которых он зависит. Это отделяет создание объектов от их использования.
IoC Контейнер (IoC Container) - это реализация IoC и DI. Контейнер IoC создает и управляет bean-компонентами на основе мета-информации. Он также может решать и предоставлять зависимости для создаваемых им объектов.
BeanDefinition - описывает bean-компоненты. Создается на основе разобранной мета-информации.
BeanFactory - это интерфейс который создает и предоставляет bean-компоненты на основе BeanDefinition-ов. Он является ядром ApplicationContext
.
ApplicationContext - это центральный интерфейс который предоставляете следующий список возможностей:
возможности BeanFactory
загрузка ресурсов
публикация событий
интернационализация
автоматическая регистрация
BeanPostProcessor
иBeanFactoryPostProcessor
BeanFactoryPostProcessor - это интерфейс, который позволяет настраивать определения bean-компонентов контекста приложения. Он создается и запускается перед BeanPostProcessor.
BeanPostProcessor - это интерфейс для обеспечения интеграции кастомной логики создания экземпляров, разрешения зависимостей и т. д. Каждый компонент, созданный BeanFactory
, проходит через каждый зарегистрированный BeanPostProcessor
.
ApplicationContextEvent - основной класс для событий, возникающих в процессе жизненного цикла ApplicationContext. Его подклассы:
ContextRefreshedEvent - публикуется автоматически после поднятия контекста
ContextStartedEvent - публикуется методом
ApplicationContext#start
ContextStoppedEvent - публикуется методом
ApplicationContext#stop
ContextClosedEvent - публикуется автоматически перед закрытием контекста
ApplicationListener - интерфейс который позволяет обрабатывать ApplicationEvent события. Можно использовать аннотацию @EventListener вместо интерфейса.
Lifecycle - интерфейс похожий на ApplicationListener
, но в нем определено 2 метода, которые срабатывают во время запуск (start) и остановку (stop) контекста.
SmartLifcycle - это расширение Lifecycle интерфейса. Отличие в том, что он срабатывает во время обновление (refresh) и закрытия (close) контекста.
Понимаю, что это может быть немного запутанным. Я постараюсь разложить все по полочкам далее.
Жизненный цикл контекста Spring-а
Жизненный цикл контекста состоит из 4-ёх этапов:
Этап обновления (refresh) - автоматический
Этап запуска (start) - вызывается методом
ApplicationContext#start
Этап остановки (stop) - вызывается методом
ApplicationContext#stop
Этап закрытия (close) - автоматический
Этап обновления контекста
BeanFactory
создаетBeanFactoryPostProcessor
-ы используя конструктор без аргументов
Стоит знать
BeanFactory
может создать экземплярBeanFactoryPostProcessor
только с конструктором без аргументов. В противном случае вы получите сообщение об ошибке со следующим сообщением:No default constructor found.
Обратные вызовы инициализации и уничтожения не работают как у обычных bean-компоненты если вы используете конфигурацию на основе аннотаций. Но они работают если использовать конфигурации на основе XML. Подробности в Жизненный цикл bean-компонента.
Если вы пометили
BeanFactoryPostProcessor
как лениво инициализируемый, тоBeanFactory
проигнорирует это
ApplicationContext
вызывает методBeanFactoryPostProcessor#postProcessBeanFactory
BeanFactory
createsBeanPostProcessor
-ы
Стоит знать
`ApplicationContext` позволяет внедрять зависимости в конструктор `BeanPostProcessor`, но такой компонент не будет обрабатываться `BeanPostProcessor` и вы получите следующее сообщение: `Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying`
Обратные вызовы инициализации и уничтожения работают как обычные bean-компоненты.
ApplicationContext
регистрируетBeanPostProcessor
-ыИнициализация singleton bean-компонентов. Подробности в Жизненный цикл bean-компонента.
ApplicationContext
проверяет флагSmartLifecycle#isRunning
и вызывает методSmartLifecycle#start
, если флаг имеет значениеfalse
Стоит знать
Метод
SmartLifecycle#start
вызывается автоматически на этапе обновления (refresh), поскольку флагSmartLifecycle#isAutoStartup
по умолчанию имеет значение trueМетод
Lifecycle#start
не вызывается на этапе обновления. Он вызывается на этапе запуска (start). Начальная фаза запускается только с помощьюApplicationContext#start
.
ApplicationContext
публикуетContextRefreshedEvent
Методы обратного вызова, помеченные аннотацией
@EventListener
с типом параметра методаContextRefreshedEvent
, обрабатывают это событие. Также здесь может бытьApplicationListener
Стоит знать
Один метод может обрабатывать несколько событий. Например:
`@EventListener(classes = { ContextStartedEvent.class, ContextStoppedEvent.class })`
Этап запуска контекста
ApplicationContext
проверяет флагLifecycle#isRunning
и вызывает методLifecycle#start
, если флаг имеет значение falseApplicationContext
проверяет флагSmartLifecycle#isRunning
и вызывает методSmartLifecycle#start
, если флаг имеет значение false. Да-да, контекст второй раз проходиться по объектам реализующие интерфейсSmartLifecycle
ApplicationContext
публикуетContextStartedEvent
Методы обратного вызова, помеченные аннотацией
@EventListener
с типом параметра методаContextStartedEvent
, обрабатывают это событие. Также здесь может бытьApplicationListener
Этап остановки контекста
ApplicationContext
проверяет флагSmartLifecycle#isRunning
и вызывает методSmartLifecycle#stop
, если флаг имеет значение trueApplicationContext
проверяет флагLifecycle#isRunning
и вызывает методLifecycle#stop
, если флаг имеет значение trueApplicationContext
публикуетContextStoppedEvent
Методы обратного вызова, помеченные аннотацией
@EventListener
с типом параметра методаContextStoppedEvent
, обрабатывают это событие. Также здесь может бытьApplicationListener
Этап закрытия контекста
ApplicationContext
публикуетContextClosedEvent
Методы обратного вызова, помеченные аннотацией
@EventListener
с типом параметра методаContextClosedEvent
, обрабатывают это событие. Также здесь может бытьApplicationListener
ApplicationContext
проверяет флагSmartLifecycle#isRunning
и вызывает методSmartLifecycle#stop
, если флаг имеет значениеtrue
Стоит знать
Это выполниться раньше если был запущен этап остановки контекста
ApplicationContext
проверяет флагLifecycle#isRunning
и вызывает методLifecycle#stop
, если флаг имеет значениеtrue
Стоит знать
Это выполниться раньше если был запущен этап остановки контекста
Уничтожение bean-компонентов. Подробности в Жизненный цикл bean-компонента
Жизненный цикл bean-компонента
Жизненный цикл bean-компонента состоит из 2-ух этапов:
Этап инициализации
Этап уничтожения
Этап инициализации bean-компонента
BeanFactory
создает bean-компонентСрабатывает статический блок инициализации
Срабатывает не статический блок инициализации
Внедрение зависимостей на основе конструктора
Внедрение зависимостей на основе
setter
-ов
Стоит знать
Если вы используете комбинацию 2 подходов конфигурирования: на основе XML и на основе аннотаций, то следует знать, что внедрение зависимостей через аннотации выполняется перед внедрением зависимостей через XML. Таким образом, конфигурация XML переопределяет аннотации для свойств.
Отрабатывают методы стандартного набора
*Aware
интерфейсовBeanPostProcessor#postProcessBeforeInitialization
обрабатывает bean-компонентInitDestroyAnnotationBeanPostProcessor#postProcessBeforeInitialization
вызывает методы обратного вызова, помеченные аннотацией@PostConstruct
BeanFactory
вызывает методInitializingBean#afterPropertiesSet
BeanFactory
вызывает метод обратного вызова, зарегистрированный какinitMethod
BeanPostProcessor#postProcessAfterInitialization
обрабатывает bean-компонент
Этап уничтожения bean-компонента
Этап уничтожения срабатывает только для singleton bean-компонентов, так как только эти компоненты храниться в BeanFactory
.
InitDestroyAnnotationBeanPostProcessor.postProcessBeforeDestruction
вызывает методы обратного вызова, отмеченные как@PreDestroy
BeanFactory
вызывает методInitializingBean#destroy
BeanFactory
вызывает метод обратного вызова, зарегистрированный какdestroyMethod
Стоит знать
По умолчанию bean-компоненты, определенные с конфигурацией Java, которые имеют public метод close()
или shutdown()
, автоматически становятся методами обратного вызова уничтожения.
Дополнения к жизненному циклу
В данном подразделе я кратко расскажу об инструментах, о которых я не рассказывал ранее, так как они позволяют добавить более специфическую логику в жизненный цикл Spring-a. А моя цель была подробно разобрать типичный жизненный цикл.
Ordered - интерфейс, позволяющий управлять порядком работы компонентов. Например, если компоненты реализуют BeanPostProcessor
/BeanFactoryPostProcessor
и Ordered
интерфейсы, то мы можем контролировать порядок их выполнения.
FactoryBean - интерфейс, позволяющий внедрить сложную логику создания объекта. Если у вас есть сложный код инициализации, который лучше выражается на Java, вы можете создать свой собственный FactoryBean
, написать сложную инициализацию внутри этого класса, а затем подключить свой собственный FactoryBean
к контейнеру.
ApplicationStartup - это инструмент, который помогает понять, на что тратится время на этапе запуска.
Еще существует механизм перезапуска приложения. Это может понадобиться в случаях:
Если нам нужно загрузить измененную мета-информацию
Если нужно изменить текущие активные профили
Если контекст упал и мы хотим иметь возможно автоматически поднять его
Подробнее об этом можно почитать в статье Programmatically Restarting a Spring Boot Application
Заключение
В этой статье мы изучили жизненный цикл Spring Framework, построили четкий алгоритм его работы. Мы изучили инструменты интеграции, которые позволяют использовать жизненный цикл в своих интересах, и выяснили роль каждого инструмента.
Эту статью вы можете использовать для подготовки к собеседованию, или как справочник, когда вы думаете как использовать инструменты Spring-а в своем проекте.
Все проверки я проводил на разработанном мною тестовом стенде, который наглядно показывает алгоритм жизненного цикла с использованием инструментов интеграции предоставляемых Spring Framework-ом.
Также у меня есть репозиторий, который тесно связан с темой этой статьи. Там вы найдете продублированный код из серии выступлений Spring-потрошитель от Евгения Борисова. Я создал этот репозиторий 4 года назад, так что facepalm гарантирую.
Спасибо за внимание и любите друг друга! Теперь это жизненно необходимо.
P.S. Да, я слизал эту фразу у Вовы Ломова ведущего канала Теплица социальных технологий.
Полезные ссылки
Комментарии (8)
ivanovdev
00.00.0000 00:00+1Интересная тема затронута, но такое ощущение что писал ее ChatGPT
lord_detson Автор
00.00.0000 00:00Не буду настаивать на том, что бы вы поверили, что это я написал на основе своих заметок в Obsidian. В я даже могу понять почему у вас может складываться такое впечатление. Я старался все показать и объяснить достаточно кратко и емко. И я думаю, что ChatGPT придерживаеться такомуже принципу.
Мне главное, что бы статья была полезная для сообщества программистов, а все остальное не важно)
Спасибо, что уделили время!
Absolutely17
00.00.0000 00:00+1В "Этап уничтожения bean-компонента" опечатка: вместо
@PreDestroy
указано@PostConstruct
dimkus
Похвала за проведённое исследование. Хотелось бы внести пару ясностей. В статье заявлен Spring Framework, но статья описывает исследование Spring Boot Framework, что является некой обёрткой для Спринга. Принцип один, но построение контекста отличается. Ну и в статье я бы сказал, что скорее описано построение контекста. Далее заявляется внедрение кастомной логики в жизнный цикл, это сама по себе плохая затея, правильнее было бы задуматься как решить поставленную задачу в рамках возможности стандартных методов и подходов фреймворка, т.к. дальнейшее обновление может сломать вашу программу. Также другой программист, который столкнётся с вашей кастомной логикой может очень долго плеваться и не понимать, что происходит. В принципе фреймворки для этого и придуманы, чтоб ускорить и упорядочить разработку типовых задач. Для лучшего понимания особенно для новичков было бы не плохо описать поведение Сервисов, Контроллеров, Репозиториев, Использование Хибернейт, Использование больше, чем одной DB одновременно, как себя ведут Транзакции, Прокси объекты, Аспекты и т.д. поле не паханное ;-)
alexdoublesmile
Вам стоит почитать, что такое Spring Boot вообще, ибо понимание нулевое.
lord_detson Автор
Спасибо большое) Вы, кстати, тоже затронули интересную тему, которую я захотел раскрыть чуть подробнее.
Я реализовал еще один тестовый стэнд, который ответил бы на вопросы:
Есть ли отличие в жизненных цыклах Spring Framework между версиями?
В версии 1.2.9 статический блок инициализации вызывается в самом начале.
Это отличается от версии 2.5.6 и более поздних версий, поскольку статический блок инициализации вызывается после
BeanFactoryPostProcessor
иBeanPostProcessor
.Методы уничтожения
BeanFactoryPostProcessor
иBeanPostProcessor
вызываются перед методами уничтожения бинов.Это самая большая разница между версиями. Более поздние версии просто добавили новые инструменты интеграции.
Я также заметил, что методы обратного вызова инициализации и уничтожения вызываются для
BeanFactoryPostProcessor
, хотя он не был в конфигурации на основе аннотаций. Это основное отличие между конфигурациями на основе XML и аннотаций.Есть ли отличия в жизненных цыклах Spring Framework и Spring Boot?
Ответ: нет, так как Spring Boot внутри себя использует тотже Spring Framework.
Стоит ли бояться изменений жизненного цикла в будущих версиях?
Можно не бояться использовать средства интеграции Spring, так как порядок жизненного цикла скорее всего не изменится в последующих версиях.
Также можно заметить, что в целом разработчики фреймворка только добавляют новые инструменты, а не изменяют старые, чтобы сохранить обратную совместимость и не создавать дополнительные боли при переходе на новую версию фреймворка.
Конечно, вы должны быть осторожны с жизненным циклом, как и с любым другим инструментом, и вести осмысленную документацию.
Более подробно вы можете прочитать перейдя по ссылке.