Вітання! Поки адміністрація JavaRush працює над новими рівнями, я хочу почати серію навчальних статей по Spring Framework. Так, я знаю, що у мережі вже багато матеріалу на цю тему, але, як показує практика, всі вони на рівні Hello World'a. Я хочу розповісти не про те, як правильно розставити анотації, а про те, як це все влаштовано «під капотом». Стаття розрахована на тих, хто вже так чи інакше працював із цим фреймворком та знайомий з основними поняттями.
Ініціалізація контексту.
Отже, почнемо з основ. На мій погляд, одним із найважливіших моментів є розуміння того, як відбувається налаштування контексту та ініціалізації бінів. Як відомо, перш ніж 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="com.codegym.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)
Справа в тому, що бін це не обов'язково клас бізнес-логіки вашої програми. Біном також називають інфраструктурний бін. Коротко, інфраструктурний бін це клас, який налаштовує біни вашої бізнес-логіки (так-так, надто багато бінів). Докладніше про нього я розповім нижче, але щоб було трохи зрозуміліше, що саме BPP налаштовує, наведу приклад. Всім же знайома інструкція@Autowired
? Так ось, саме AutowiredAnnotationBeanPostProcessor
відповідальний за те, щоб усі ваші класи були запроваджені один в одного.
Повернімося до 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
-Методу. Отже, ще раз алгоритм ініціалізації контексту:
XmlBeanDefinitionReader
сканує наш XML-конфігураційний файл.- Створює
BeanDefinition
і кладе їх уHashMap
. - Приходить
BeanFactory
і з цієїHashMap
окремо складає всіBeanPostProcessor
ти. - Створює з '
BeanDefinition
ів біни і кладе їх у IoC-контейнер. - Тут приходять BPP та налаштовують ці біни за допомогою 2х методів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ