Spring Framework является одним из самых сложных фремворков для понимания и изучения. Большинство разработчиков изучают его медленно, через практические задачи и гугл. Этот подход не эффективен, так как не даёт полной картины и при этом требует больших затрат.
Я хотел бы предложить вам принципиально новый подход к изучению Спринга. Он заключается в том, что человек проходит через серию специально подготовленных туториалов и самостоятельно реализует функционал спринга. Особенность этого подхода в том, что он, помимо 100%-го понимания изучаемых аспектов Spring даёт ещё большой прирост в Java Core (Annotations, Reflection, Files, Generics).
Статья подарит вам незабываемые ощущения и позволит почувствовать себя разработчиком Pivotal. Шаг за шагом, вы сделаете ваши классы бинами и организуете их жизненный цикл (такой же, как и в реальном спринге). Классы, которые вы будете реализовывать — BeanFactory, Component, Service, BeanPostProcessor, BeanNameAware, BeanFactoryAware, InitializingBean, PostConstruct, PreDestroy, DisposableBean, ApplicationContext, ApplicationListener, ContextClosedEvent.
Немного о себе
Меня зовут Ярослав, и я Java Developer с 4-х летним опытом работы. На данный момент я работаю в компании EPAM Systems (СПБ), и с интересом углубляюсь в те технологии, которые мы используем. Довольно часто приходится иметь дело со спрингом, и я вижу в нём некоторую золотую середину, в которой можно разиваться (Java все итак нормально знают, а слишком специфические инструменты и технологии могут приходить и уходить).
Пару месяцев назад я прошёл сертификацию Spring Professional v5.0 (без прохождения курсов). После этого я задумался над тем, как можно обучать спрингу других людей. К сожалению, на данный момент нет эффективной методики обучения. У большинства разработчиков складывается весьма поверхностное представление о фреймворке и его особенностях. Дебажить исходники спринга слишком тяжело и абсолютно не эффективно с точки зрения обучения (я как-то увлекался этим). Сделать 10 проектов? Да, вы где-то сможете углубить свои знания и получите много практического опыта, но многое из того, что «под капотом», так и не откроется перед вами. Читать книгу Spring in Action? Круто, но затратно по усилиям. Я вот проработал её 40% (во время подготовки к сертификации), но это было не просто.
Единственный способ понять что-то до конца — самостоятельно разработать это. Недавно у меня появилась идея о том, что можно провести человека через интересный туториал, который будет курировать разработку своего DI-фреймворка. Главная его особенность будет заключаться в том, что API будет совпадать с изучаемым API. Офигенность данного подхода в том, что помимо глубокого (без пробелов) понимания спринга, человек получит ОГРОМНОЕ количество опыта по Java Core. Признаюсь честно, я сам много всего нового узнал во время подготовки статьи, как по Spring, так и по Java Core. Давайте приступим к разработке!
Проект с нуля
Итак, первое, что нужно сделать — это открыть любимую IDE и создать проект с чистого листа. Никаких Maven, никаких сторонних библиотек мы подключать не будем. Даже Spring-зависимости подключать не будем. Наша цель — разработать API, максимально похожий на Spring API, и реализовать его самостоятельно.
В чистом проекте создайте 2 главных пакета. Первый пакет — ваше приложение (
com.kciray
), и класс Main.java
внутри него. Второй пакет — org.springframework. Да, мы будем дублировать структуру пакетов оригинального спринга, название его классов и их методов. Есть такой интересный эффект — когда вы создаете что-то свое, это свое начинает казаться простым и понятным. Потом, когда вы будете работать в больших проектах, вам будет казаться, что там все создано на основе вашей заготовки. Такой подход может очень положительно сказаться на понимании работы системы в целом, её улучшении, исправлении багов, решении проблем и так далее.Если будут вознить какие-то проблемы, работающий проект можете взять тут.
Создаём контейнер
Для начала, поставим задачу. Представим, что у нас есть 2 класса —
ProductFacade
и PromotionService
. Теперь представим, что вы хотите связать эти классы между собой, но так, чтобы сами классы не знали друг о друге (Паттерн DI). Нужен какой-то отдельный класс, который будет управлять всеми этими классами и определять зависимости между ними. Назовём его контейнер. Создадим класс Container
… Хотя нет, подождите! В Spring нету единого класса-контейнера. У нас есть много реализаций контейнеров, и все эти реализации можно разделить на 2 типа — фабрики бинов и контексты. Фабрика бинов создаёт бины и связывает их между собой (инъекция зависимостей, DI), а контекст делает примерно то же самое, плюс ещё добавляет некоторые дополнительные функции (например, интернационализация сообщений). Но эти дополнительные функции нам не нужны сейчас, поэтому будем работать с фабрикой бинов.Создайте новый класс
BeanFactory
и поместите его в пакет org.springframework.beans.factory
. Пускай внутри этого класса хранится Map<String, Object> singletons
, в которой id
бина замапено на сам бин. Добавьте к нему метод Object getBean(String beanName)
, который вытаскивает бины по идентификатору.public class BeanFactory {
private Map<String, Object> singletons = new HashMap();
public Object getBean(String beanName){
return singletons.get(beanName);
}
}
Обратите внимание на то, что
BeanFactory
и FactoryBean
— это разные вещи. Первое — это фабрика бинов (контейнер), а второе — это бин-фабрика, который сидит внутри контейнера и тоже производит бины. Фабрика внутри фабрики. Если вы путаетесь между этими определениями, можете запомнить, что в английском языке второе существительное является ведущим, а первое — служит чем-то типа прилагательного. В слове BeanFactory Главным словом является фабрика, а в FactoryBean — бин.Теперь, создадим классы
ProductService
и PromotionsService
. ProductService
будет возвращать продукт из БД, но перед этим нужно проверить, применимы ли к этому продукту какие-либо скидки (Promotions). В электронной коммерции работу со скидками часто выделяют в отдельный класс-сервис (а иногда и в сторонний веб-сервис).public class PromotionsService {
}
public class ProductService {
private PromotionsService promotionsService;
public PromotionsService getPromotionsService() {
return promotionsService;
}
public void setPromotionsService(PromotionsService promotionsService) {
this.promotionsService = promotionsService;
}
}
Теперь нам надо сделать так, чтобы наш контейнер (
BeanFactory
) обнаружил наши классы, создал их за нас и инжектировал один в другой. Операции типа new ProductService()
должны находится внутри контейнера и делаться за разработчика. Давайте используем самый современный подход (сканирование классов и аннотации). Для этого нам нужно ручками создать аннотацию @Component
(пакет org.springframework.beans.factory.stereotype
).@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
По умолчанию аннотации не загружаются в память во время работы программы (
RetentionPolicy.CLASS
). Мы изменили данное поведение через новую политику удержания (RetentionPolicy.RUNTIME
).Теперь добавьте
@Component
перед классами ProductService
и перед PromotionService
.@Component
public class ProductService {
//...
}
@Component
public class PromotionService {
//...
}
Нам нужно, чтобы
BeanFactory
сканировал наш пакет (com.kciray
) и находил в нем классы, которые аннотированы @Component
. Эта задача совсем не тривиальная. В Java Core нет готового решения, и нам придётся делать костыль самому. Тысячи приложений на спринге используют сканирование компонентов через этот костыль. Вы узнали страшную правду. Вам придется извлекать из ClassLoader
названия файлов и проверять, заканчиваются они на ".class" или нет, а потом строить их полное имя и вытаскивать по нему объекты классов!Сразу хочу предупредить, что будет много проверяемых исключений, поэтому будьте готовы их оборачивать. Но для начала, давайте определимся, чего мы хотим. Мы хотим добавить специальный метод в
BeanFactory
и вызывать его в Main
://BeanFactory.java
public class BeanFactory{
public void instantiate(String basePackage) {
}
}
//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
Далее, нам нужно получить
ClassLoader
. Он отвечает за загрузку классов, и добывается довольно просто:ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Наверно вы уже заметили, что пакеты разделяются точкой, а файлы — прямым слешем. Нам надо преобразовать пакетный путь в путь к папке, и получить что-то типа
List<URL>
(пути в вашей файловой системе, по которым можно искать class-файлы).String path = basePackage.replace('.', '/'); //"com.kciray" -> "com/kciray"
Enumeration<URL> resources = classLoader.getResources(path);
Так, подождите!
Enumeration<URL>
это не List<URL>
. Что это вообще такое? О ужас, это же старый прародитель Iterator
, доступный ещё с времен Java 1.0. Это легаси, с которым нам приходится иметь дело. Если по Iterable
можно пройтись с помощью for (все коллекции его реализуют), то в случае Enumeration
вам придётся делать обход ручками, через while(resources.hasMoreElements())
и nextElement()
. И ещё там нет возможности удалять элементы из коллекции. Только 1996 год, только хардкор. Ах да, в Java 9 добавили метод Enumeration.asIterator()
, так что можете работать через него.Поехали дальше. Нам надо извлечь папки и проработать содержимое каждой из них. Преобразуем URL в файл, а затем получаем его имя. Тут надо отметить, что мы не будем сканировать вложенные пакеты, чтобы не усложнять код. Можете усложнить себе задачу и сделать рекурсию, если есть желание.
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File file = new File(resource.toURI());
for(File classFile : file.listFiles()){
String fileName = classFile.getName();//ProductService.class
}
}
Дальше, нам нужно получить название файла без расширения. На дворе 2018 год, Java много лет развивала File I/O (NIO 2), но до сих пор не может отделить расширение от имени файла. Приходится свой велосипед создавать, т.к. мы решили не использовать сторонние библиотеки вроде Apache Commons. Давайте используем старый дедовский способ
lastIndexOf(".")
:if(fileName.endsWith(".class")){
String className = fileName.substring(0, fileName.lastIndexOf("."));
}
Далее, мы можем по полному имени класса получить объект класса (для этого вызываем класс класса
Class
):Class classObject = Class.forName(basePackage + "." + className);
Окей, теперь наши классы в наших руках. Далее, осталось только выделить среди них те, что имеют аннотацию
@Component
:if(classObject.isAnnotationPresent(Component.class)){
System.out.println("Component: " + classObject);
}
Запустите и проверьте. В консоли должно быть что-то вроде этого:
Component: class com.kciray.ProductService
Component: class com.kciray.PromotionsService
Теперь нам нужно создать наш бин. Надо сделать что-то вроде
new ProductService()
, но для каждого бина у нас свой класс. Рефлексия в Java предоставляет нам универсальное решение (вызывается конструктор по-умолчанию):Object instance = classObject.newInstance();//=new CustomClass()
Далее, нам нужно поместить этот бин в
Map<String, Object> singletons
. Для этого нужно выбрать имя бина (его id). В Java мы называем переменные подобно классам (только первая буква в нижнем регистре). Данный подход может быть применим к бинам тоже, ведь Spring — это Java-фреймворк! Преобразуйте имя бина так, чтобы первая буква была маленькая, и добавьте его в мапу:String beanName = className.substring(0, 1).toLowerCase() + className.substring(1);
singletons.put(beanName, instance);
Теперь убедитесь в том, что всё работает. Контейнер должен создавать бины, и они должны извлекаться по имени. Обратите внимание на то, что название вашего метода
instantiate()
и название метода classObject.newInstance();
имеют общий корень. Более того, instantiate()
— это часть жизненного цикла бина. В джаве всё взаимосвязано!//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
ProductService productService = (ProductService) beanFactory.getBean("productService");
System.out.println(productService);//ProductService@612
Попробуйте также реализовать аннотацию
org.springframework.beans.factory.stereotype.Service
. Она выполняет абсолютно ту же функцию, что и @Component
, но называется по-другому. Весь смысл заключён в названии — вы демонстриуете, что класс является сервисом, а не просто компонентом. Это что-то типа концептуальной типизации. В сертификации по спрингу был вопрос «Какие аннотации являются стереотипными? (из перечисленных)». Так вот, стереотипные аннотации — это те, которые находятся в пакете stereotype
. Наполняем свойства
Посмотрите на схему ниже, на ней представлено начало жизненного цикла бина. То, что мы делали до этого, это Instantiate (создание бинов через
newInstance()
). Следующий этап — это перекрестное инжектирование бинов (инъекция зависимостей, она же инверсия контроля (IoC)). Нужно пройтись по свойствам бинов и понять, какие именно свойства нужно заинжектить. Если вы сейчас вызовете productService.getPromotionsService()
, то получите null
, т.к. зависимость ещё не добавлена.Для начала, создадим пакет
org.springframework.beans.factory.annotation
и добавим в него аннотацию @Autowired
. Идея в том, чтобы помечать этой аннотацией те поля, которые являются зависимостями.@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
Далее, добавим её к свойству:
@Component
public class ProductService {
@Autowired
PromotionsService promotionsService;
//...
}
Теперь нам нужно научить наш
BeanFactory
находить эти аннотации и инжектировать зависимости по ним. Добавим отдельный метод для этого, и вызовем его из Main
:public class BeanFactory {
//...
public void populateProperties(){
System.out.println("==populateProperties==");
}
}
Далее, нам нужно всего-лишь пройтись по всем нашим бинам в мапе
singletons
, и для каждого бина пройтись по всем его полям (метод object.getClass().getDeclaredFields()
возвращает все поля, включая приватные). И проверить, есть ли у поля аннотация @Autowired
:for (Object object : singletons.values()) {
for (Field field : object.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
}
}
}
Далее, нам надо ещё разок пройтись по всем бинам и посмотреть их тип — вдруг это тот тип, который хочет себе взять наш бин. Да, у нас получается трехмерный цикл!
for (Object dependency : singletons.values()) {
if (dependency.getClass().equals(field.getType())) {
}
}
Далее, когда мы нашли зависимость, надо её заинжектить. Первое что вам может прийти в голову — это записать поле
promotionsService
с помощью рефлексии напрямую. Но спринг так не работает. Ведь если поле имеет модификатор private
, то нам придется сначала установить его как public
, потом записать наше значение, потом снова установить в private
(чтобы сохранить целостность). Звучит как большой костыль. Давайте вместо большого костыля сделаем маленький костыль (сформируем название сеттера и вызовем его):String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);//setPromotionsService
System.out.println("Setter name = " + setterName);
Method setter = object.getClass().getMethod(setterName, dependency.getClass());
setter.invoke(object, dependency);
Теперь запустите ваш проект и убедитесь, что при вызове
productService.getPromotionsService()
вместо null
возвращается наш бин. То, что мы реализовали — это инъекция по типу. Есть ещё инъекция по имени (аннотация
javax.annotation.Resource
). Отличается она тем, что вместо типа поля будет извлекаться его имя, и по нему — зависимость из мапы. Тут всё аналогично, даже в чем-то проще. Я рекомендую вам поэкспериментировать и создать какой-нибудь свой бин, а потом заинжектить его с помощью @Resource
и расширить метод populateProperties()
. Поддерживаем бины, знающие о своем имени
Бывают случаи, когда внутри бина нужно получить его имя. Такая потребность возникает не часто, т.к. бины, по своей сути, не должны знать друг о друге и о том, что они бины. В первых версиях спринга предполагалось, что бин — это POJO (Plain Old Java Objec, старый добрый Джава-объект), а вся конфигурация вынесена в XML-файлы и отделена от реализации. Но мы реализуем данный функционал, так как инъекция имени — это часть жизненного цикла бина.
Как нам узнать, какой бин хочет узнать, как его зовут, а какой не хочет? Первое, что приходит в голову — это сделать новую аннотацию типа
@InjectName
и лепить её на поля типа String. Но это решение будет слишком общим и позволяет выстрелить себе в ногу много раз (разместить эту аннотацию на полях неподходящих типов (не String), или же пытаться инжектировать имя в несколько полей в одном классе). Есть другое решение, более аккуратное — создать специальный интерфейс с одним методом-сеттером. Все бины, что его реализуют — получает своё имя. Создайте класс BeanNameAware
в пакете org.springframework.beans.factory
:public interface BeanNameAware {
void setBeanName(String name);
}
Далее, пускай наш
PromotionsService
его реализует:@Component
public class PromotionsService implements BeanNameAware {
private String beanName;
@Override
public void setBeanName(String name) {
beanName = name;
}
public String getBeanName() {
return beanName;
}
}
И, наконец, добавим новый метод в фабрику бинов. Тут всё просто — мы проходимся по нашим бинам-синглтонам, проверяем, реализует ли бин наш интерфейс, и вызываем сеттер:
public void injectBeanNames(){
for (String name : singletons.keySet()) {
Object bean = singletons.get(name);
if(bean instanceof BeanNameAware){
((BeanNameAware) bean).setBeanName(name);
}
}
}
Запустите и убедиесь, что всё работает:
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
beanFactory.populateProperties();
beanFactory.injectBeanNames();
//...
System.out.println("Bean name = " + promotionsService.getBeanName());
Надо отметить, что в спринге есть и другие подобные интерфейсы. Я рекомендую вам самостоятельно реализовать интерфейс BeanFactoryAware, который позволит бинам получать ссылку на фабрику бинов. Реализуется он аналогично.
Инициализируем бины
Представим, что у вас возникла ситуация, когда нужно выполнить некоторый код после того, как зависимости были проинжектированы (свойства бина установлены). Говоря простым языком, нам нужно предоставить бину возможность инициализировать самого себя. Как вариант, мы можем создать интерфейс
InitializingBean
, и в него поместить сигнатуру метода void afterPropertiesSet()
. Реализация данного механизма абсолютно аналогична той, что была представлена для интерфейса BeanNameAware
, поэтому решение под спойлером. Потренируйтесь и сделайте его самостоятельно за минуту:Решение для инициализации бина
//InitializingBean.java
package org.springframework.beans.factory;
public interface InitializingBean {
void afterPropertiesSet();
}
//BeanFactory.java
public void initializeBeans(){
for (Object bean : singletons.values()) {
if(bean instanceof InitializingBean){
((InitializingBean) bean).afterPropertiesSet();
}
}
}
//Main.java
beanFactory.initializeBeans();
Добавляем пост-процессоры
Представьте себя на месте первых разработчиков спринга. Ваш фреймворк растёт и пользуется огромной популярностью среди девелоперов, на почту каждый день приходят письма с просьбами добавить ту или иную полезную фичу. Если для каждой такой фичи добавлять свой собственный интерфейс и проверять его в жизенном цикле бина, то он (жизненный цикл) окажется засоренным ненужной информацией. Вместо этого, мы можем создать один универсальный интерфейс, который позволит добавлять выполнение некоторой логики (абсолютно любой, будь то проверка на аннотацию, замена бина на другой бин, установка некоторых особых свойств и так далее).
Давайте подумаем, для чего предназначен данный интерфейс. Он должен производить некоторую пост-обработку бинов, следовательно его можно назвать BeanPostProcessor. Но перед нами стоит непростой вопрос — когда следует выполнять логику? Ведь мы можем выполнить её до инициализации, а можем выполнить и после. Для одних задач лучше подходит первый вариант, для других — второй… Как быть?
Мы можем позволить оба варианта сразу. Пускай один пост-процессор несёт две логики, два метода. Один выполняется до инициализации (до метода
afterPropertiesSet()
), а другой — после. Теперь давайте задумаемся над самими методами — какие параметры у них должны быть? Очевидно, что там должен быть сам бин (Object bean
). Для удобства, кроме бина можно передавать имя этого бина. Вы же помните, что бин сам по себе не знает о своём имени. И мы не хотим заставлять все бины реализовывать интерфейс BeanNameAware. Но, на уровне пост-процессора, имя бина может очень даже пригодиться. Поэтом удобавляем его как второй параметр. А что должен возвращать метод при пост-обработке бина? Сделаем так, чтобы он возвращал сам бин. Это даёт нам супер-гибкость, ведь вместо бина можно подсунуть прокси-объект, который оборачивает его вызовы (и добавляет секьюрити). А можно и вовсе вернуть другой объект, пересоздав бин заново. Разработчикам даётся очень большая свобода действия. Ниже представлена окончательная версия спроектированного интерфейса:
package org.springframework.beans.factory.config;
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName);
Object postProcessAfterInitialization(Object bean, String beanName);
}
Далее, нам нужно добавить список из прост-процессоров к нашей фабрике бинов и возможность добавлять новые. Да, это обычный ArrayList.
//BeanFactory.java
private List<BeanPostProcessor> postProcessors = new ArrayList<>();
public void addPostProcessor(BeanPostProcessor postProcessor){
postProcessors.add(postProcessor);
}
Теперь поменяем метод
initializeBeans
так, чтобы он учитывал пост-процессоры:public void initializeBeans() {
for (String name : singletons.keySet()) {
Object bean = singletons.get(name);
for (BeanPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeforeInitialization(bean, name);
}
if (bean instanceof InitializingBean) {
((InitializingBean) bean).afterPropertiesSet();
}
for (BeanPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessAfterInitialization(bean, name);
}
}
}
Давайте создадим небольшой пост-процессор, который просто трассирует вызовы в консоль, и добавим его в нашу фабрику бинов:
public class CustomPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("---CustomPostProcessor Before " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("---CustomPostProcessor After " + beanName);
return bean;
}
}
//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.addPostProcessor(new CustomPostProcessor());
Теперь запустите и убедитесь, что всё работает. В качестве тренировочного задания создайте пост-процессор, который будет обеспечивать работу аннотации
@PostConstruct (javax.annotation.PostConstruct)
. Она предоставляет альтернативный способ инициализации (имеющий корни в Java, а не в спринге). Суть его в том, что вы размещаете аннотацию на некотором методе, и этот метод будет вызван ПЕРЕД стандартной спринговой инициализацией (InitializingBean). Обязательно создавайте все аннотации и пакеты (даже javax.annotation) вручную, не подключайте зависимости! Это поможет вам увидеть разницу между ядром спринга и его расширениями (поддержка javax), и запомнить её. Это позволит придерживаться одного стиля в будущем.
Вам будет интересен тот факт, что в реальном спринге аннотация
@PostConstruct
именно так и реализована, через пост-процессор CommonAnnotationBeanPostProcessor. Но не подглядывайте туда, напишите свою реализацию.На последок, я вам рекомендую добавить метод
void close()
в класс BeanFactory
и отработать ещё два механизма. Первый — аннотация @PreDestroy (javax.annotation.PreDestroy)
, предназначена для методов, которые должны быть вызваны при закрытии контейнера. Второй — интерфейс org.springframework.beans.factory.DisposableBean
, который содержит метод void destroy()
. Все бины, исполняющие данный интерфейс, будут иметь возможность сами себя уничтожить (освободить ресурсы, например).@PreDestroy + DisposableBean
//DisposableBean.java
package org.springframework.beans.factory;
public interface DisposableBean {
void destroy();
}
//PreDestroy.java
package javax.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface PreDestroy {
}
//DisposableBean.java
public void close() {
for (Object bean : singletons.values()) {
for (Method method : bean.getClass().getMethods()) {
if (method.isAnnotationPresent(PreDestroy.class)) {
try {
method.invoke(bean);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
if (bean instanceof DisposableBean) {
((DisposableBean) bean).destroy();
}
}
}
Полный жизненный цикл бина
Вот мы и реализовали полный жизненный цикл бина, в его современном виде. Я надеюсь, что такой подход поможет вам его запомнить.
Наш любимый контекст
Программисты очень часто используют термин контекст, но не все понимают, что же он на самом деле значит. Сейчас мы расставим всё по-полочкам. Как я уже отметил в начале статьи, контекст — это реализация контейнера, как и
BeanFactory
. Но, кроме базовых функций (DI), она ещё добавляет некоторые крутые фичи. Одна из таких фич — это отправка и обработка событий между бинами. Статья получилась слишком большой и содержимое стало обрезаться, поэтому я поместил информацию по контексту под спойлер.
Реализуем контект
Начнем с заготовки контекста. Создайте пакет
Добавьте его в класс
Теперь давайте подумаем, как организовать события. Поскольку у нас уже есть метод
Теперь нам надо создать интерфейс
Теперь вернёмся к классу
Но нет. Тут возникает трудность. Мы НЕ МОЖЕМ сделать проверку типа
На самом деле, информация о типах сохраняется в метаданных класса, и очень даже успешно извлекается через рефлексию. Для начала, нам нужно получить список из интерфейсов, которые данный бин реализует, и отфильтровать среди них те, которые имеют параметры:
Далее, мы всего лишь извлекаем тип первого параметра, и убеждаемся, что он — наш класс события. Если это так, мы получаем наш метод через рефлексию и вызываем его:
Пускай один из ваших классов реализует интерфейс ApplicationListener:
Далее, тестируете ваш контекст в Main и убеждаетесь, что он также работает, и событие отправляется:
org.springframework.context
, и класс ApplicationContext
внутри него. Пусть он содержит внутри себя экземпляр класса BeanFactory
. Все этапы инициализации поместим в конструктор, а также добавим перенаправление метода close()
.public class ApplicationContext {
private BeanFactory beanFactory = new BeanFactory();
public ApplicationContext(String basePackage) throws ReflectiveOperationException{
System.out.println("******Context is under construction******");
beanFactory.instantiate(basePackage);
beanFactory.populateProperties();
beanFactory.injectBeanNames();
beanFactory.initializeBeans();
}
public void close(){
beanFactory.close();
}
}
Добавьте его в класс
Main
, запустите и убедитесь, что он работает:ApplicationContext applicationContext = new ApplicationContext("com.kciray");
applicationContext.close();
Теперь давайте подумаем, как организовать события. Поскольку у нас уже есть метод
close()
, мы можем создать событие «Закрытие контекста» и перехватить его внутри какого-нибудь бина. Создайте простой класс, представляющий данное событие:package org.springframework.context.event;
public class ContextClosedEvent {
}
Теперь нам надо создать интерфейс
ApplicationListener
, который позволит бинам слушать наши события. Поскольку мы решили представлять события в виде классов, то имеет смысл типизировать этот интерфейс по классу события (ApplicationListener<E>
). Да, мы будем использовать Java-дженерики, и вы получите немножко опыта по работе с ними. Далее, вам нужно придумать название для метода, который будет обрабатывать событие:package org.springframework.context;
public interface ApplicationListener<E>{
void onApplicationEvent(E event);
}
Теперь вернёмся к классу
ApplicationContext
. Нам нужно в методе close()
пройтись по всем нашим бинам, и выяснить, какие из них являются слушателями событий. Если бин заимплементил ApplicationListener<ContextClosedEvent>
, значит нужно вызвать его onApplicationEvent(ContextClosedEvent)
. Кажется просто и логично, не так ли?public void close(){
beanFactory.close();
for(Object bean : beanFactory.getSingletons().values()) {
if (bean instanceof ApplicationListener) {
}
}
}
Но нет. Тут возникает трудность. Мы НЕ МОЖЕМ сделать проверку типа
bean instanceof ApplicationListener<ContextClosedEvent>
. Это связано с особенностью реализации Java. При компиляции происходит так называемая очистка типов (type erasure), при которой все <T> заменяются на <Object>. Как же быть, что же делать? Как нам выловить бины, которые имплементят именно ApplicationListener<ContextClosedEvent>
, а не другие типы событий?На самом деле, информация о типах сохраняется в метаданных класса, и очень даже успешно извлекается через рефлексию. Для начала, нам нужно получить список из интерфейсов, которые данный бин реализует, и отфильтровать среди них те, которые имеют параметры:
for (Type type: bean.getClass().getGenericInterfaces()){
if(type instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) type;
}
}
Далее, мы всего лишь извлекаем тип первого параметра, и убеждаемся, что он — наш класс события. Если это так, мы получаем наш метод через рефлексию и вызываем его:
Type firstParameter = parameterizedType.getActualTypeArguments()[0];
if(firstParameter.equals(ContextClosedEvent.class)){
Method method = bean.getClass().getMethod("onApplicationEvent", ContextClosedEvent.class);
method.invoke(bean, new ContextClosedEvent());
}
Пускай один из ваших классов реализует интерфейс ApplicationListener:
@Service
public class PromotionsService implements BeanNameAware, ApplicationListener<ContextClosedEvent> {
//...
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println(">> ContextClosed EVENT");
}
}
Далее, тестируете ваш контекст в Main и убеждаетесь, что он также работает, и событие отправляется:
//Main.java
void testContext() throws ReflectiveOperationException{
ApplicationContext applicationContext = new ApplicationContext("com.kciray");
applicationContext.close();
}
Заключение
Изначально я планировал данную статью для Baeldung на английском, но потом подумал, что аудитория хабры может положительно оценить данный подход к обучению. Если вам понравились мои идеи, обязательно поддержите статью. Если она наберёт рейтинг более 30, то обещаю продолжение. При написании статьи, я старался показать именно те знания Spring Core, которе используются наиболее часто, а также с опорой на Core Spring 5.0 Certification Study Guide. В будущем, с помощью таких туториалов можно покрыть всю сертификацию и сделать спринг более доступным для Java-разработчиков.
Update 10/05/2018
Мне постоянно приходят письма с вопросами «а когда продолжение, мы его ждём». Но вот времени совсем нету, и другие личные проекты в приоритете. Однако, если кому-то из вас действительно понравилась идея, вы можете изучить узкий раздел спринга и написать статью-продолжение. Если у вас нету аккаунта хабры, то я могу опубликовать статью от моего акка или помочь вам получить инвайт.
Распределение тем:
Spring Container — [имя пользователя]
Spring AOP — [имя пользователя]
Spring Web — [имя пользователя]
Spring Cloud — [имя пользователя]
Only registered users can participate in poll. Log in, please.
Тема для следующей статьи
50.43% Spring Container, углубляемся ещё больше (@Bean, @Configuration, Context, Prototype, XML)116
11.3% Spring AOP, пишем прокси своими руками (@Aspect, @Pointcut)26
29.13% Spring Web, веб-сервер с нуля (@Contoller, @RequestMapping)67
2.61% Spring WebFlux6
6.52% Spring Cloud15
230 users voted. 30 users abstained.