Введение

Ни для кого не секрет, что 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) контекста.

SmartLifecycle - это расширение Lifecycle интерфейса. Отличие в том, что он срабатывает во время обновления (refresh) и закрытия (close) контекста.

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

Жизненный цикл контекста Spring-а

Жизненный цикл контекста состоит из 4-ёх этапов:

  1. Этап обновления (refresh) - автоматический

  2. Этап запуска (start) - вызывается методом ApplicationContext#start

  3. Этап остановки (stop) - вызывается методом ApplicationContext#stop

  4. Этап закрытия (close) - автоматический

Этап обновления контекста

  1. BeanFactory создает BeanFactoryPostProcessor-ы используя конструктор без аргументов

Стоит знать
  • BeanFactory может создать экземпляр BeanFactoryPostProcessor только с конструктором без аргументов. В противном случае вы получите сообщение об ошибке со следующим сообщением: No default constructor found.

  • Обратные вызовы инициализации и уничтожения не работают как у обычных bean-компоненты если вы используете конфигурацию на основе аннотаций. Но они работают если использовать конфигурации на основе XML. Подробности в Жизненный цикл bean-компонента.

  • Если вы пометили BeanFactoryPostProcessor как лениво инициализируемый, то BeanFactory проигнорирует это

  1. ApplicationContext вызывает метод BeanFactoryPostProcessor#postProcessBeanFactory

  2. BeanFactory creates BeanPostProcessor

Стоит знать
  • `ApplicationContext` позволяет внедрять зависимости в конструктор `BeanPostProcessor`, но такой компонент не будет обрабатываться `BeanPostProcessor` и вы получите следующее сообщение: `Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying`

  • Обратные вызовы инициализации и уничтожения работают как обычные bean-компоненты.

  1. ApplicationContext регистрирует BeanPostProcessor

  2. Инициализация singleton bean-компонентов. Подробности в Жизненный цикл bean-компонента.

  3. ApplicationContext проверяет флаг SmartLifecycle#isRunning и вызывает метод SmartLifecycle#start, если флаг имеет значение false

Стоит знать
  • Метод SmartLifecycle#start вызывается автоматически на этапе обновления (refresh), поскольку флаг SmartLifecycle#isAutoStartup по умолчанию имеет значение true

  • Метод Lifecycle#startне вызывается на этапе обновления. Он вызывается на этапе запуска (start). Начальная фаза запускается только с помощью ApplicationContext#start.

  1. ApplicationContext публикует ContextRefreshedEvent

  2. Методы обратного вызова, помеченные аннотацией @EventListener с типом параметра метода ContextRefreshedEvent, обрабатывают это событие. Также здесь может быть ApplicationListener

Стоит знать

Один метод может обрабатывать несколько событий. Например:

`@EventListener(classes = { ContextStartedEvent.class, ContextStoppedEvent.class })`

Этап запуска контекста

  1. ApplicationContext проверяет флаг Lifecycle#isRunning и вызывает метод Lifecycle#start, если флаг имеет значение false

  2. ApplicationContext проверяет флаг SmartLifecycle#isRunning и вызывает метод SmartLifecycle#start, если флаг имеет значение false. Да-да, контекст второй раз проходиться по объектам реализующие интерфейс SmartLifecycle

  3. ApplicationContext публикует ContextStartedEvent

  4. Методы обратного вызова, помеченные аннотацией @EventListener с типом параметра метода ContextStartedEvent, обрабатывают это событие. Также здесь может быть ApplicationListener

Этап остановки контекста

  1. ApplicationContext проверяет флаг SmartLifecycle#isRunning и вызывает метод SmartLifecycle#stop, если флаг имеет значение true

  2. ApplicationContext проверяет флаг Lifecycle#isRunning и вызывает метод Lifecycle#stop, если флаг имеет значение true

  3. ApplicationContext публикует ContextStoppedEvent

  4. Методы обратного вызова, помеченные аннотацией @EventListener с типом параметра метода ContextStoppedEvent, обрабатывают это событие. Также здесь может быть ApplicationListener

Этап закрытия контекста

  1. ApplicationContext публикует ContextClosedEvent

  2. Методы обратного вызова, помеченные аннотацией @EventListener с типом параметра метода ContextClosedEvent, обрабатывают это событие. Также здесь может быть ApplicationListener

  3. ApplicationContext проверяет флаг SmartLifecycle#isRunning и вызывает метод SmartLifecycle#stop, если флаг имеет значение true

Стоит знать

Это выполниться раньше если был запущен этап остановки контекста

  1. ApplicationContext проверяет флаг Lifecycle#isRunning и вызывает метод Lifecycle#stop, если флаг имеет значение true

Стоит знать

Это выполниться раньше если был запущен этап остановки контекста

  1. Уничтожение bean-компонентов. Подробности в Жизненный цикл bean-компонента

Жизненный цикл bean-компонента

Жизненный цикл bean-компонента состоит из 2-ух этапов:

  1. Этап инициализации

  2. Этап уничтожения

Этап инициализации bean-компонента

  1. BeanFactory создает bean-компонент

  2. Срабатывает статический блок инициализации

  3. Срабатывает не статический блок инициализации

  4. Внедрение зависимостей на основе конструктора

  5. Внедрение зависимостей на основе setter-ов

Стоит знать

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

  1. Отрабатывают методы стандартного набора *Aware интерфейсов

  2. BeanPostProcessor#postProcessBeforeInitialization обрабатывает bean-компонент

  3. InitDestroyAnnotationBeanPostProcessor#postProcessBeforeInitialization вызывает методы обратного вызова, помеченные аннотацией @PostConstruct

  4. BeanFactory вызывает метод InitializingBean#afterPropertiesSet

  5. BeanFactory вызывает метод обратного вызова, зарегистрированный как initMethod

  6. BeanPostProcessor#postProcessAfterInitialization обрабатывает bean-компонент

Этап уничтожения bean-компонента

Этап уничтожения срабатывает только для singleton bean-компонентов, так как только эти компоненты храниться в BeanFactory.

  1. InitDestroyAnnotationBeanPostProcessor.postProcessBeforeDestruction вызывает методы обратного вызова, отмеченные как@PreDestroy

  2. BeanFactory вызывает метод InitializingBean#destroy

  3. 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. Да, я слизал эту фразу у Вовы Ломова ведущего канала Теплица социальных технологий.

Полезные ссылки