Конспект Spring-потрошителя
Короткий и не очень подробный конспект выступлений Евгения Борисова
Оригинальные видео #
Краткое описание #
Видео-материал предлагает несколько полезных примеров, объясняющих работу фреймворка Spring. Для полноценного изучения вопроса рекомендую всё-таки обратиться к документации, но в качестве исходного материала подойдёт.
XmlBeanDefinitionReader #
XmlBeanDefinitionReader сканирует applicationContext.xml и создаёт на основании этого файла объект, реализующий интерфейс ApplicationContext. Одним из наиболее частых вариантов является класс ClassPathXmlApplicationContext.
По сути ApplicationContext представляет из себя HashMap, где ключами являются названия бинов, а в качестве значений содержится описание контекста их создания.
После создания ApplicationContext BeanFactory начинает по очереди обрабатывать описанные бины из HashMap’ы. Создаваемые бины складываются в IoC-контейнер (IoC - Inversion of control).
В контейнер кладутся только Singleton бины. Поэтому нужно помнить, что все OnDestroy методы будут работать только для синглтон-бинов, потому что Спринг знает о них - они лежат в контейнере. Все прочие бины с другими временами жизни (например, Prototype) просто отдаётся на обработку в запросивший их бин и их жизненный цикл далее не контролируется.
BeanPostProcessor #
Позволяет настраивать бины до того, как они попадают в контейнер.
BeanPostProcessor имеет доступ к аннотациям, описанным на классах с помощью механизма Reflection. Поэтому BeanPostProcessor’ы могут использоваться для настройки Спринга, обучения его новому функционалу.
Для создания нового BeanPostProcessor необходимо создать класс, который имплементирует интерфейс BeanPostProcessor. Интерфейс содержит два метода:
- postProcessBeforeInitialization(Object bean, String beanName)
- postProcessAfterInitialization(Object bean, String beanName)
При модификации классов с использованием Reflection часто приходится обрабатывать исключения. Для того, чтобы избежать ручной обработки исключений можно использовать пакет ReflectionUtils, который поставляется вместе со Spring.
Init-method #
Spring представляет возможность написания отдельной функции, которая будет инициализировать бин. Устаревшим способом считается определение метода afterPropertiesSet(). В настоящее время используются варианты определения через параметр init-method или аналогичную ему аннотацию @PostConstruct.
Двухфазовый конструктор #
Вопрос: зачем нужны инит-методы, есть же конструктор? Ответ: возможность использования двухфазового конструктора.
Как Спринг создаёт бины:
- Просканировали контекст;
- Создались BeanDefinition’ы;
- BeanFactory читает весь контекст и откладывает в сторонку BeanPostProcessor’ы
- Понял, что нужно создать такой-то бин(ы);
- При помощи Reflection запустил его конструктор;
- Конструктор отработал, объект создался, а дальше может настраивать;
- BeforeInitialization
- Инит-методы
- AfterInitialization
Прокси #
Вопрос: нафига два прохода по BeanPostProcessor’ам (before и after)? Ответ: можно делать прокси.
Разбираем на примере кастомной аннотации @Profiling. Положим, что нам нужно сделать свою аннотацию, которая будет для указанного класса подменять все его методы на аналогичные с бизнес точки зрения, но выполняющие профилирование при своей работе. Профилирование будем реализовывать по схеме:
- Запоминаем текущее время.
- Вызываем оригинальный метод.
- Вычитаем из текущего времени предыдущее запомненное.
При этом никто не должен заметить, что мы подменили объект прокси. Есть, например, два варианта как можно реализовать такое поведение:
- Наследовать от оригинального класса (dynamic proxy);
- CJlib - создаётся аналогичный интерфейс.
Те BeanPostProcessor’ы, которые что-то меняют, например создают прокси, должны это делать на этапе afterInitialization, так как иначе придётся делать прокси-объект, который точно поддерживает аналогичную оригинальному классу инициализацию, а это сложно и не всегда нужно.
Поскольку на этапе beforeInitialization и при инициализации BeanDefinition и объект могут измениться настолько, что мы утратим информацию об аннотациях, которые хотели применить на этапе afterInitialization, нам нужно запомнить в самом начале все бины, которые на нужно будет поменять на afterInitialization. Для этого используем значение поля beanName. beanName никогда не меняется, поэтому на этапе beforeInitialization можем запоминать классы, на которых надо что-то сделать. Таким образом мы запоминаем оригинальные классы.
Дальше в видео идёт речь про проксирование и про JMX Console. Думаю, что тут можно не описывать, поскольку тема сторонняя. Очень интересная, не спорю, поэтому отсылаю к оригинальному видео.
Application Listener или трехфазовый конструктор #
Трёхфазовый конструктор может понадобиться в случае, когда необходимо сделать нечто с классом уже после того, как он был полностью настроен. Хороший пример - прогрев кэша некоторого объекта. Этот метод надо вызывать только после того, как в класс проинжектнули все зависимости. При этом, поскольку аннотация @Transactional реализована через прокси, на этапе PostConstruct методы с транзакциями всё ещё не настроены. В этой ситуации и поможет третья фаза конструктора.
В ходе инициализации контейнера спринг формирует информацию о стадиях инициализации в форме Event’ов. На эти события можно подписаться реализовав интерфейс ApplicationListener. Из любого ивента можно вытащить контекст:
- ContextStartedEvent - контекст начал своё построение
- ContextStoppedEvent
- ContextRefreshedEvent - контекст закончил своё построение
- ContextClosedEvent
Интерфейс ApplicationListener<E> - это дженерик, куда можно в качестве дженерика можно указать конкретный ивент, который необходимо слушать.
BeanFactoryPostProcessor #
BeanFactoryPostProcessor позволяет настраивать BeanDefinition ещё до того как создаются бины.
Например, при создании бина можно переменные окружения встраивать в BeanDefinition.
Есть один единственный метод в интерфейсе BeanFactoryPostProcessor: postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory).
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
String[] names = beanFactory.getBeanDefinitionNames();
for (String name: names) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name);
String beanClassName = beanDefinition.getBeanClassName();
try {
Class<?> beanClass = Class.forName(beanClassName);
DeprecatedClass = beanClass.getAnnotation(DeprecatedClass.class);
if (annotation != null) {
beanDefinition.setBeanClassName(annotation.newImpl().getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Дополняем предыдущий лист “Как Спринг создаёт бины”:
- Просканировали контекст;
- Создались BeanDefinition’ы;
- Поработали BeanFactoryPostProcessor’ы;
- BeanFactory читает весь контекст и откладывает в сторонку BeanPostProcessor’ы
- Понял, что нужно создать такой-то бин(ы);
- При помощи Reflection запустил его конструктор;
- Конструктор отработал, объект создался, а дальше может настраивать;
- BeforeInitialization;
- Инит-методы;
- AfterInitialization.
Пишем свой ApplicationContext для properties-Files #
Тут не особо интересно…
Scope #
Скоуп бина определяет то, каким образом Spring будет создавать этот бин. По умолчанию все бины являются синглтон бинами, то есть для каждого класса создаётся только один объект, который инжектиться в другие бины.
Вторым по популярности (наверное) скоупом является прототип. Прототип бин создаётся каждый раз новый, когда у другого бина появляется необходимость получить объект этого бина.
Если даже инжектить прототип в синглтон бин, то он будет инжектиться всего один раз при создании синглтон бина.
Это можно обойти, как вариант, указанием proxyMode = ScopedProxyMode.TARGET_CLASS для аннотации @Scope. Но это не самый лучший метод, поскольку в таком случае при обращении к бину всегда будет создаваться новый объект, а это далеко не всегда нужно.
Более правильный вариант с точки зрения автора - перенести запрос требуемого ресурса из бизнес-класса в конфигурацию. В видео класс переделывают в абстрактный класс с абстрактным методом получения необходимого ресурса, который уже при конфигурации доопредяют нужным методом.
Регистрация нового Scope #
Нужно имплементировать интерфейс Scope, собственно, дальше пример, но в целом всё довольно просто.