Привет! Пока администрация JavaRush работает над новыми уровнями я хочу начать серию обучающих статей по Spring Framework. Да, я знаю, что в сети уже много материала на эту тему, но, как показывает практика, все они на уровне Hello World’a. Я же хочу рассказать не о том, как правильно расставить аннотации, а о том, как это все устроено «под капотом». Статья рассчитана на тех, кто уже так или иначе работал с этим фреймворком и знаком с основными понятиями. Spring Framework. Введение - 1

Инициализация контекста.

Итак, начнем с основ. На мой взгляд, одним из наиважнейших моментов является понимание того, как происходит настройка контекста и инициализаци бинов. Как известно, прежде чем Spring начнет работать его необходимо сконфигурировать. В допотопные времена это делали с помощью xml файлов (на некоторых проектах, преимущественно старых, продолжают делать это до сих пор). Вот небольшой пример такого конфигурационного файла:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="helloWorld" class="ru.javarush.HelloWorld">
       <property name="message" value="Hello World!"/>
   </bean>

</beans>
По-большому счету, этого достаточно, чтобы создать пару контроллеров и запустить стартап (который не взлетит). Но как эта конфигурация заставит работать Spring? А вот тут начинается самое интересно. Для того чтобы наша конфигурация была понята Spring’ом существует XmlBeanDefinitionReader. Это внутренний компонент Spring’a, который сканирует (парсит) xml и на основе того, что мы там написали создает BeanDefinition’ы. BeanDefinition – это объект, который хранит в себе информацию о бине. Сюда входит: из какого класса его (бин) надо создать; scope; установлена ли ленивая инициализация; нужно ли перед данным бином инициализировать другой и иные проперти, которые описаны в xml. Все полученные BeanDefinition’ы складываются в HashMap, в которой идентификатором является имя бина (установленное вами или присвоенное спрингом) и сам BeanDefinition объект. После того, как все BeanDefinition’ы созданы на сцену выходит новый герой – BeanFactory. Этот объект итерируется по HashMap’e с BeanDefinition’ами, создает на их основе бины и складывает в IoC контейнер. Здесь есть нюанс, на самом деле, при старте приложения, в IoC контейнер попадут бины, которые имеют scope Singleton (устанавливается по-умолчанию), остальные же создаются, тогда когда они нужны (prototype, request, session). А теперь небольшое отступление, познакомимся с еще одним персонажем.

Встречайте — BeanPostProcessor. (BPP)

bean post processorДело в том, что бин это не обязательно класс бизнес-логики вашего приложения. Бином также называют инфраструктурный бин. Вкратце, инфраструктурный бин это класс, который настраивает бины вашей бизнес-логики (да-да, слишком много бинов). Подробнее о нём я расскажу ниже, но чтобы было чуточку понятнее, что именно BPP настраивает, приведу пример. Всем же знакома аннотация @Autowired? Так вот, именно AutowiredAnnotationBeanPostProcessor ответственен за то, чтобы все ваши классы были внедрены друг в друга. uknowimean

Вернемся к BeanFactory

Зная теперь о BPP, нужно уточнить, что итерируясь по HashMap’e с BeanDefinition’ами сперва создаются и кладутся отдельно (не в IoC контейнер) все BeanPostProcessor’ы. После этого создаются обычные бины нашей бизнес-логики, складываются в IoC-контейнер и начинается их настройка с помощью отдельно отложенных BPP. А происходит это вот как, каждый BPP имеет 2 метода:

postProcessorBeforeInitialization(Object bean, String beanName);
postProcessorAfterInitialization(Object bean, String beanName);
Происходит итерация по нашим бинам дважды. В первый раз вызывается метод postProcessorBeforeInitialization, а во второй раз вызывается метод postProcessorAfterInitialization. Наверняка возник вопрос, зачем нужны два метода, объясняю. Дело в том, что для обработки некоторых аннотаций (таких как @Transactional, например) наш бин заменяется proxy классом. Чтобы понять зачем это делается, нужно знать как работает @Transactional, а работает это вот как. В метод, помеченный данной аннотацией необходимо налету добавить еще пару строк кода. Как это сделать? Верно, с помощью создания класса proxy, внутри которого и будет добавлен необходимый код в нужный метод. А теперь представим такую ситуацию, у нас есть класс:

class A {
    @Autowired
    private SomeClass someClass;

    @Transactional
    public void method() {
        // модификатор доступа обязательно public
    }
}
В этом классе 2 аннотации @Autowired и @Transactional. Обе аннотации обрабатываются разными BPP. Если первым отработает AutowiredBPP, то все будет в порядке, но если нет, то мы столкнемся с вот какой проблемой. Дело в том, что когда создается класс proxy, то вся мета-информация теряется. Другими словами, информации об аннотации @Autowired в proxy классе не будет, а значит и AutowiredBPP не отработает, а значит наше поле someClass будет иметь значение null, что, скорее всего, приведет к NPE. Также стоит знать, что между вызовами методов postProcessorBeforeInitialization и postProcessorAfterInitialization происходит вызов init-метода, если он есть. Это по-большому счету второй конструктор, но отличие в том, что в этот момент все наши зависимости уже внедрены в класс и мы можем к ним обратиться из init-метода. Итак, еще раз алгоритм инициализации контекста:
  1. XmlBeanDefinitionReader сканирует наш xml-конфигурационный файл.
  2. Создает BeanDefinition’ы и кладет их в HashMap.
  3. Приходит BeanFactory и из этой HashMap отдельно складывает все BeanPostProcessor’ы.
  4. Создает из BeanDefinition’ов бины и кладет их в IoC-контейнер.
  5. Тут приходят BPP и настраивают эти бины с помощью 2х методов.
  6. Готово.
Собственно, на этом всё, пишите понравилась вам статья и стоит ли продолжать писать подобные туториалы.