Общее представление о контейнерах и бинах в Spring

В этой статье я хочу базово пройтись по Spring. Рассказать о возможностях конфигурации её бинов и немного залезть во внутрь.

IoC container — это контейнер, реализующий принцип Inversion of Control (IoC). Он управляет созданием, связыванием и жизненным циклом бинов, которые конфигурируются на различных этапах сборки приложения и затем добавляются в контекст.

org.springframework.beans и org.springframework.context пакеты являются основой для Spring Framework«s IoC container. BeanFactory — это интерфейс контейнера Spring, предоставляющий базовый функционал для создания и управления бинами. BeanFactory используется в основном для простых приложений и в случаях, когда ресурсы ограничены. Это наиболее низкоуровневый интерфейс, предоставляющий базовые возможности по конфигурации и управлению бинами. ApplicationContext является под-интерфейсом BeanFactory. Он добавляет:

  • Упрощенную интеграцию со Spring AOP, что позволяет добавлять проксирование

  • Механизмы работы с интернационализацией, что позволяет разработчикам проще создавать приложения на различных языках

  • Механизмы публикации и обработки событий, что позволяет компонентам приложения общаться между собой (благодаря этому компоненты приложения станут более гибкими и менее связанными)

  • Свои специальные реализации:

    • WebApplicationContext: Расширяет стандартный ApplicationContext и добавляет функции, необходимые для работы с веб-приложениями, такие как поддержка сервлетов, фильтров, и интеграция с веб-сессиями

    • AnnotationConfigApplicationContext: Используется для конфигурации на основе аннотаций и Java-кода, что позволяет работать без XML-конфигурации

    • GenericWebApplicationContext: Позволяет использовать как традиционную конфигурацию на основе XML, так и конфигурацию на основе аннотаций и Java-кода

    • ClassPathXmlApplicationContext: Позволяет конфигурировать бины на основе вашего XML файла

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

На диаграмме ниже показано высокоуровневое представление о работе контейнера:

Общее представление о работе контейнера

Общее представление о работе контейнера

Как видно из диаграммы, Spring контейнер требует декларацию каждого бина. Эта декларация описывается при помощи аннотаций или XML. В ней описано метаданные для конфигурации бина (из какого класса нужно создавать, есть ли у него init () метод и как он называется, какие у него property, scope и далее). Метаданные бина Spring должен знать для того, чтобы его собрать.

Контейнер IoC Spring полностью независим от формата, в котором записаны метаданные конфигурации. Существует несколько способов настроить метаданные бинов:

  • Конфигурация на основе аннотаций: прямо в классе аннотациями размечать для Spring данные о будущем бине (@Component, @Service, @Repository, @Controller, @Autowired, @Qualifier)

    @Service 
    public class ExampleService {
        private int someValue;
      
        public void someMethod(){
          System.out.println("I'm a service");
        }
        public int getSomeValue() {
          return someValue;
        }
      
       public void setSomeValue(int someValue) {
         this.someValue = someValue;
       }
    }
    @Component 
    public class ExampleClass {
      private int value;
      
      @Autowired
      private ExampleService service;
      
      public void exampleMethod(){
        System.out.println("Hello world!");
        System.out.println("Service value: "+service.getSomeValue());
      }
      
      public int getValue() {
        return value;
      }
      
      public void setValue(int value) {
        this.value = value;
      }
    }
  • Конфигурация на основе Java: для настройки метаданных о бине нужно создать отдельный конфигурационный класс (пометить его аннотацией @Configuration), а бины регистрировать при помощи метода (помеченного аннотацией @Bean), который возвращает объект нужного класса

    @Configuration 
    public class ConfigurationClass {
      @Bean
      public ExampleService service(){
        return new ExampleService();
      }
      
      @Bean
      public ExampleClass exampleClass(){
        return new ExampleClass(service());
      }
    }
  • XML конфигурация: все бины и их зависимости прописываются в XML файле

     
    
    
    
    
      
    

У каждого бина есть свой жизненный цикл (ЖЦ). Это время от момента создания и до уничтожения его из контекста.

BeanDefenition — объект, который хранит информацию про бин

BeanPostProcessor (BPP) — позволяет настраивать бины до того как они попали в IoC контейнер (он управляет аннотациями @Autowired, @Transactional, @Async и т.д.)

BeanFactoryPostProcessor (BFPP) — класс, который позволяет настраивать BeanDefenition (т.е. еще до того как BeanFactory начала создавать бины). Может что-то изменять как в BeanDefenition, так и в самом BeanFactory перед тем, как он начнет работать и создавать бины из BeanDefenition.

Init() метод — метод, который выполняется до попадания бина в IoC контейнер (после первого и до второго прохода по всем BPP). Можно прописать:

  • Если через xml, то через атрибут «init-method» в теге

  • Если работа с аннотациями, то поставить @PostConstract

Зачем использовать init-метод если есть конструктор? Потому что конструктор вызывается и используется BeanFactory для создания объекта и базовой инициализации данных. Однако, spring начнет дополнительно настраивать поля (например, какую-либо аннотацию для поля начнет обрабатывать BPP). Возникнет проблема если в конструкторе попытаться обратиться к данным, которые настраивает Spring уже после использования самого конструктора.

В Spring инициализация бина может проходить в три фазы:

Три этапа конструирования бина

Три этапа конструирования бина

ApplicationListener — слушает events контекста, и принимает соответствующие действия
Виды событий:

  • ContextStartedEvent (контекст начал свое построение, а когда заканчивает, то делает refresh)

  • ContextStoppedEvent (контекст приложения останавливается)

  • ContextRefreshedEvent (контекст был инициализирован или обновлен)

  • ContexClosedEvent (контекст приложения закрывается)

Зачем он нужен? Пример:
Во время старта приложения нужно «разогреть» кэш (для этого нужно сходить в БД, взять данные и обновить их у себя).
В конструкторе логику не написать, потому что на этапе использования конструктора бин еще вообще не готов, в init-метод не написать, потому что на этом этапе еще не
существует транзакции (@Transaction не будет настроена, т.к. init-метод сработает раньше, чем BPP настроит аннотацию). Остается дождаться полного создания контекста и прослушать событие refreshed.

Этапы создания бина:

  1. Считываются все декларации бинов и кладет в специальную Map »BeanDefenitions» (id бина = декларация)

  2. BeanFactoryPostProcessor изменяет BeanDefenitions или BeanFactory (если настроено)

  3. После создания BeanDefenitions, BeanFactory начинает по ним работать

    1. Берет бин

    2. Настраивает его согласно конфигурации

    3. Отсылает по очереди ко всем BeanPostProcessor (BPP), потому что могут быть как кастомные, так и от spring (с аннотациями @Autowired, @Async, @Transactional и т.д.)

    4. У объекта вызывается init () метод

    5. Еще один проход по всем BPP (на случай проксирования)

    6. Кладет в IoC контейнер (если scope бина singleton)

Создание бинов на примере XML конфигурации

Создание бинов на примере XML конфигурации

Так же, у каждого бина можно создать destroy-метод, который сработает перед его уничтожением. Можно прописать:

  • Если работа с аннотациями, то над методом поставить @PreDestroy

  • Если через xml, то через атрибут «destroy-method » в теге

Заключение

Хочешь, хорошо работать — пользуйся Спрингом
Хочешь, чтобы работало хорошо — знай его кишки

Евгений Борисов на вебинаре «Спринг Потрошитель»

© Habrahabr.ru