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), она ещё добавляет некоторые крутые фичи. Одна из таких фич — это отправка и обработка событий между бинами.

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

Реализуем контект
Начнем с заготовки контекста. Создайте пакет 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-разработчиков.

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


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

Комментарии (14)


  1. igor_suhorukov
    30.08.2018 15:50

    IMHO мало пользы от такого велосипеде написания. Как вариант обучения — дополнять Spring Framework, Spring Boot тестами. Это и пользу сообществу принесет и позволит глубже погрузиться в этот проект. Поверьте мне, что проекту нужна помощь сообщества и борьба с накопившимся за десятилетие техническим долгом.


  1. cepro
    31.08.2018 00:13

    Большое спасибо за отличный материал!
    Не слушайте скептиков, делайте продолжение.


  1. trix
    31.08.2018 00:22

    да, собственная реализация — отличный подход для понимания внутренностей чего-либо.
    к сожалению, повторяя спринг без критического взгляда на его концептуальное устройства вы повторяете его со всеми присущими ему граблями, даже не задумываясь — типа, раз в спринге так сделано, значит так и надо делать :)

    например, те же аннотации, которые команда спринга притащила, чтобы уйти от xml и протолкнула в j2ee и многие другие фреймворки, в итоге везде вокруг имеем это печальное зрелище


    1. AstarothAst
      31.08.2018 08:28

      например, те же аннотации, которые команда спринга притащила, чтобы уйти от xml и протолкнула в j2ee и многие другие фреймворки, в итоге везде вокруг имеем это печальное зрелище

      Какое «это»? С того момента, как я отказался в своих проектах от xml я еще ни разу не пожалел о своем решении, так что с моей точки зрения аннотации тут безусловное благо.


      1. trix
        31.08.2018 12:16

        «Это» — использование аннотаций для решения любых фантазий, несмотря на то, что они жестко привязывают код к конкретной реализации контейнера, несмотря на то, что содержание аннотаций невозможно валидировать на этапе компиляции и тяжело рефакторить и т.д. и т.п.

        Разумеется, повальное увлечение аннотациями началось раньше спринга, но благодаря спрингу теперь большинство людей просто в упор не видят альтернативы в области DI конфигурации. То есть или xml или аннотации, судя по вашему комментарию.

        Самое смешное, что спринг внутри поддерживает вайринг неаннотированных классов. Добавить обертку для красоты, и можно пользоваться, создавая контейнер в чистом ява-коде (нет, Java Based Configuration — это не то же самое) Но изначальный выбор спринга в пользу member injection вместо constructor injection, а потом такая же вредная идея с автосканом компонентов создала, закрепила и распространила на все подпроекты спринга именно такой подход.


        1. AstarothAst
          31.08.2018 13:47

          Не совсем понял аргументацию, если честно.

          но благодаря спрингу теперь большинство людей просто в упор не видят альтернативы в области DI конфигурации. То есть или xml или аннотации, судя по вашему комментарию.

          Или java-конфиг, который более гибкий, нежели xml и более читаемый. Ну, и да, большинство людей за простоту, а что может быть проще написать Component над классом?

          Самое смешное, что спринг внутри поддерживает вайринг неаннотированных классов. Добавить обертку для красоты, и можно пользоваться, создавая контейнер в чистом ява-коде (нет, Java Based Configuration — это не то же самое)

          Чем «добавленная обертка» лучше Java Based Configuration? Или чем лучше одной аннотации @_Component?

          Но изначальный выбор спринга в пользу member injection вместо constructor injection, а потом такая же вредная идея с автосканом компонентов создала, закрепила и распространила на все подпроекты спринга именно такой подход.

          В документации к Спрингу создатели сами призывают пользоваться constructor injection. А автоскан-то чем не угодил???


          1. trix
            31.08.2018 14:54

            Они к constructor injection призывать начали относительно поздно. И это реально стало «поздно», потому что в целом это редко выполняется даже в рамках проектов под зонтиком spring.

            Автоскан оставляет вас без единой точки конфигурации, чем сложнее проект, тем тяжелее вам понять как взаимосвязаны компоненты, что находится в контексте, а что нет. Приходится использовать костыли IDE.
            А если у вас не один контекст на приложение, а множество динамически создаваемых иерархических контейнеров разграничивающих области видимости?

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


            1. AstarothAst
              31.08.2018 15:48

              Они к constructor injection призывать начали относительно поздно. И это реально стало «поздно», потому что в целом это редко выполняется даже в рамках проектов под зонтиком spring.

              Мое мнение, что если программист сам не пришел к тому, что инжект через конструктор безусловное благо, и продолжает ляпать инжект сеттерами, то никакие призывы ему не помогут. Это нужно просто осознать. То есть тут не проблема Спринга, как такового, я считаю.

              Автоскан оставляет вас без единой точки конфигурации, чем сложнее проект, тем тяжелее вам понять как взаимосвязаны компоненты, что находится в контексте, а что нет. Приходится использовать костыли IDE.

              Почему же «костыли»? Тулинг уже давно стал неотъемлемой составляющей разработки, времена «я буду смотреть код в блокноте» прошли (я надеюсь!), и сейчас рулит правило «нет тулинга — нет фреймворка». Ну, и я по-началу как раз пытался обеспечить себе такую «единую точку конфигурации», но в итоге пришел к выводу, что овчинка выделки не стоит. Особенно, если конфигурация в xml…

              А если у вас не один контекст на приложение, а множество динамически создаваемых иерархических контейнеров разграничивающих области видимости?

              Тогда ограничения фреймворка либо не помогут, либо будут настолько кабальными, чт придется брать другой фреймворк :) Да и часто ли такое надо? Спринг все же мейнстрим, должен подходить основной массе разработчиков, а у основной массы таких задач, как мне кажется, нет.

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

              То есть мы руками сделали то, что Спринг прекрасно делает автоматически, руководствуясь аннотациями? :) По-моему сомнительный профит…


  1. HelloWord
    31.08.2018 10:49
    +1

    Спасибо за статью! Немного напоминает творения Борисова потрошителя, но все равно здорово. Ждём продолжения!


  1. Barrya42
    31.08.2018 10:49
    +1

    Интересный материал и метод преподнесения, плюсанул бы но карма не позволяет).
    Хотелось бы продолжения серии статей.
    Спасибо за труд.


  1. Shiftuia
    31.08.2018 10:49
    +1

    Автор, выражаю огромную благодарность за такое оформление и подачу материала!
    Было очень интересно узнать, что в Spring находится под капотом простым и понятным языком. С нетерпением жду следующей статьи.


  1. nebular
    31.08.2018 12:43

    Отличная статья, жду продолжения


  1. alle_gro
    31.08.2018 16:42

    Да было интересно. Спасибо. Ждем продолжения.


  1. debose
    01.09.2018 20:40

    Большое спасибо. Я примерно представлял как оно работает, но не думал что всё настолько просто.