Введение

Ни для кого не секрет, что 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-ёх этапов:

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

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

Комментарии (8)


  1. dimkus
    00.00.0000 00:00
    +4

    Похвала за проведённое исследование. Хотелось бы внести пару ясностей. В статье заявлен Spring Framework, но статья описывает исследование Spring Boot Framework, что является некой обёрткой для Спринга. Принцип один, но построение контекста отличается. Ну и в статье я бы сказал, что скорее описано построение контекста. Далее заявляется внедрение кастомной логики в жизнный цикл, это сама по себе плохая затея, правильнее было бы задуматься как решить поставленную задачу в рамках возможности стандартных методов и подходов фреймворка, т.к. дальнейшее обновление может сломать вашу программу. Также другой программист, который столкнётся с вашей кастомной логикой может очень долго плеваться и не понимать, что происходит. В принципе фреймворки для этого и придуманы, чтоб ускорить и упорядочить разработку типовых задач. Для лучшего понимания особенно для новичков было бы не плохо описать поведение Сервисов, Контроллеров, Репозиториев, Использование Хибернейт, Использование больше, чем одной DB одновременно, как себя ведут Транзакции, Прокси объекты, Аспекты и т.д. поле не паханное ;-)


    1. alexdoublesmile
      00.00.0000 00:00
      +1

      Вам стоит почитать, что такое Spring Boot вообще, ибо понимание нулевое.


    1. lord_detson Автор
      00.00.0000 00:00
      +1

      Спасибо большое) Вы, кстати, тоже затронули интересную тему, которую я захотел раскрыть чуть подробнее.

      Я реализовал еще один тестовый стэнд, который ответил бы на вопросы:

      Есть ли отличие в жизненных цыклах Spring Framework между версиями?

      В версии 1.2.9 статический блок инициализации вызывается в самом начале.
      Это отличается от версии 2.5.6 и более поздних версий, поскольку статический блок инициализации вызывается после BeanFactoryPostProcessor и BeanPostProcessor.

      Методы уничтожения BeanFactoryPostProcessor и BeanPostProcessor вызываются перед методами уничтожения бинов.

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

      Я также заметил, что методы обратного вызова инициализации и уничтожения вызываются для BeanFactoryPostProcessor, хотя он не был в конфигурации на основе аннотаций. Это основное отличие между конфигурациями на основе XML и аннотаций.

      Есть ли отличия в жизненных цыклах Spring Framework и Spring Boot?

      Ответ: нет, так как Spring Boot внутри себя использует тотже Spring Framework.

      Стоит ли бояться изменений жизненного цикла в будущих версиях?

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

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

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

      Более подробно вы можете прочитать перейдя по ссылке.


  1. ivanovdev
    00.00.0000 00:00
    +1

    Интересная тема затронута, но такое ощущение что писал ее ChatGPT


    1. lord_detson Автор
      00.00.0000 00:00

      Не буду настаивать на том, что бы вы поверили, что это я написал на основе своих заметок в Obsidian. В я даже могу понять почему у вас может складываться такое впечатление. Я старался все показать и объяснить достаточно кратко и емко. И я думаю, что ChatGPT придерживаеться такомуже принципу.

      Мне главное, что бы статья была полезная для сообщества программистов, а все остальное не важно)

      Спасибо, что уделили время!


  1. sergey_russkih89
    00.00.0000 00:00
    +1

    Отличная шпаргалка по фреймворку! Добавил в закладки.


  1. Absolutely17
    00.00.0000 00:00
    +1

    В "Этап уничтожения bean-компонента" опечатка: вместо @PreDestroy указано @PostConstruct


    1. lord_detson Автор
      00.00.0000 00:00

      Да, вы правы. Большое спасибо за подсказку! Я исправил это)