Хабрахабр

Реализация Spring Framework API с нуля. Пошаговое руководство для начинающих. Часть 1

Большинство разработчиков изучают его медленно, через практические задачи и гугл. 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 все итак нормально знают, а слишком специфические инструменты и технологии могут приходить и уходить).

0 (без прохождения курсов). Пару месяцев назад я прошёл сертификацию Spring Professional v5. К сожалению, на данный момент нет эффективной методики обучения. После этого я задумался над тем, как можно обучать спрингу других людей. Дебажить исходники спринга слишком тяжело и абсолютно не эффективно с точки зрения обучения (я как-то увлекался этим). У большинства разработчиков складывается весьма поверхностное представление о фреймворке и его особенностях. Да, вы где-то сможете углубить свои знания и получите много практического опыта, но многое из того, что «под капотом», так и не откроется перед вами. Сделать 10 проектов? Круто, но затратно по усилиям. Читать книгу Spring in Action? Я вот проработал её 40% (во время подготовки к сертификации), но это было не просто.

Недавно у меня появилась идея о том, что можно провести человека через интересный туториал, который будет курировать разработку своего DI-фреймворка. Единственный способ понять что-то до конца — самостоятельно разработать это. Офигенность данного подхода в том, что помимо глубокого (без пробелов) понимания спринга, человек получит ОГРОМНОЕ количество опыта по Java Core. Главная его особенность будет заключаться в том, что API будет совпадать с изучаемым API. Давайте приступим к разработке! Признаюсь честно, я сам много всего нового узнал во время подготовки статьи, как по Spring, так и по Java Core.

Проект с нуля

Итак, первое, что нужно сделать — это открыть любимую IDE и создать проект с чистого листа. Никаких Maven, никаких сторонних библиотек мы подключать не будем. Даже Spring-зависимости подключать не будем. Наша цель — разработать API, максимально похожий на Spring API, и реализовать его самостоятельно.

Первый пакет — ваше приложение (com.kciray), и класс Main.java внутри него. В чистом проекте создайте 2 главных пакета. Да, мы будем дублировать структуру пакетов оригинального спринга, название его классов и их методов. Второй пакет — org.springframework. Потом, когда вы будете работать в больших проектах, вам будет казаться, что там все создано на основе вашей заготовки. Есть такой интересный эффект — когда вы создаете что-то свое, это свое начинает казаться простым и понятным. Такой подход может очень положительно сказаться на понимании работы системы в целом, её улучшении, исправлении багов, решении проблем и так далее.

Если будут вознить какие-то проблемы, работающий проект можете взять тут.

Создаём контейнер

Для начала, поставим задачу. Представим, что у нас есть 2 класса — ProductFacade и PromotionService. Теперь представим, что вы хотите связать эти классы между собой, но так, чтобы сами классы не знали друг о друге (Паттерн DI). Нужен какой-то отдельный класс, который будет управлять всеми этими классами и определять зависимости между ними. Назовём его контейнер. Создадим класс Container… Хотя нет, подождите! В Spring нету единого класса-контейнера. У нас есть много реализаций контейнеров, и все эти реализации можно разделить на 2 типа — фабрики бинов и контексты. Фабрика бинов создаёт бины и связывает их между собой (инъекция зависимостей, DI), а контекст делает примерно то же самое, плюс ещё добавляет некоторые дополнительные функции (например, интернационализация сообщений). Но эти дополнительные функции нам не нужны сейчас, поэтому будем работать с фабрикой бинов.

Пускай внутри этого класса хранится Map<String, Object> singletons, в которой id бина замапено на сам бин. Создайте новый класс BeanFactory и поместите его в пакет org.springframework.beans.factory. Добавьте к нему метод Object getBean(String beanName), который вытаскивает бины по идентификатору.

public class BeanFactory
}

Обратите внимание на то, что BeanFactory и FactoryBean — это разные вещи. Первое — это фабрика бинов (контейнер), а второе — это бин-фабрика, который сидит внутри контейнера и тоже производит бины. Фабрика внутри фабрики. Если вы путаетесь между этими определениями, можете запомнить, что в английском языке второе существительное является ведущим, а первое — служит чем-то типа прилагательного. В слове BeanFactory Главным словом является фабрика, а в FactoryBean — бин.

ProductService будет возвращать продукт из БД, но перед этим нужно проверить, применимы ли к этому продукту какие-либо скидки (Promotions). Теперь, создадим классы ProductService и PromotionsService. В электронной коммерции работу со скидками часто выделяют в отдельный класс-сервис (а иногда и в сторонний веб-сервис).

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

Service. Попробуйте также реализовать аннотацию org.springframework.beans.factory.stereotype. Весь смысл заключён в названии — вы демонстриуете, что класс является сервисом, а не просто компонентом. Она выполняет абсолютно ту же функцию, что и @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, который позволит бинам получать ссылку на фабрику бинов. Реализуется он аналогично.

Инициализируем бины

Говоря простым языком, нам нужно предоставить бину возможность инициализировать самого себя. Представим, что у вас возникла ситуация, когда нужно выполнить некоторый код после того, как зависимости были проинжектированы (свойства бина установлены). Реализация данного механизма абсолютно аналогична той, что была представлена для интерфейса BeanNameAware, поэтому решение под спойлером. Как вариант, мы можем создать интерфейс InitializingBean, и в него поместить сигнатуру метода void afterPropertiesSet(). Потренируйтесь и сделайте его самостоятельно за минуту:

Решение для инициализации бина

//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. Теперь запустите и убедитесь, что всё работает. Она предоставляет альтернативный способ инициализации (имеющий корни в Java, а не в спринге). PostConstruct). Суть его в том, что вы размещаете аннотацию на некотором методе, и этот метод будет вызван ПЕРЕД стандартной спринговой инициализацией (InitializingBean).

Это поможет вам увидеть разницу между ядром спринга и его расширениями (поддержка javax), и запомнить её. Обязательно создавайте все аннотации и пакеты (даже javax.annotation) вручную, не подключайте зависимости! Это позволит придерживаться одного стиля в будущем.

Но не подглядывайте туда, напишите свою реализацию. Вам будет интересен тот факт, что в реальном спринге аннотация @PostConstruct именно так и реализована, через пост-процессор CommonAnnotationBeanPostProcessor.

Первый — аннотация @PreDestroy (javax.annotation. На последок, я вам рекомендую добавить метод void close() в класс BeanFactory и отработать ещё два механизма. Второй — интерфейс org.springframework.beans.factory. PreDestroy), предназначена для методов, которые должны быть вызваны при закрытии контейнера. Все бины, исполняющие данный интерфейс, будут иметь возможность сами себя уничтожить (освободить ресурсы, например). 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), она ещё добавляет некоторые крутые фичи. Одна из таких фич — это отправка и обработка событий между бинами.

Статья получилась слишком большой и содержимое стало обрезаться, поэтому я поместил информацию по контексту под спойлер.

Реализуем контект

Начнем с заготовки контекста. Создайте пакет 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), при которой все заменяются на . Как же быть, что же делать? Как нам выловить бины, которые имплементят именно 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-разработчиков.

Требуются Java-разработчики! (Санкт-Петербург)

Как я уже сказал, я работаю в компании EPAM Systems, в отделении электронной коммерции. На наших стримах высоко ценят квалифицированных разработчиков, и мы запланировали большое расширение в этом году. Нам требуются разработчики, нормально знающие Java/Spring, владеющие английским хотя-бы на уровне B1, и готовые развиваться и изучать одну из самых крутых систем электронной коммерции SAP Hybris (менторинг программа 1.5 месяца). Территориально мы находимся в 7 мин. от метро Горьковская. Я могу вызвать вас на собеседование в любой момент и рекомендовать в наш отдел. Если кого заинтересовало — за подробностями в личку, резюме скидывать на kciray8@gmail.com.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть