JavaRush /Java блог /Random /Краткий экскурс в внедрение зависимостей или "Что ещё за ...
Viacheslav
3 уровень

Краткий экскурс в внедрение зависимостей или "Что ещё за CDI?"

Статья из группы Random
Основой, на которой сейчас построены самые популярные фрэймворки, является внедрение зависимостей. Предлагаю посмотреть, что про это говорится в спецификации CDI, какие базовые возможности у нас есть и как ими можно воспользоваться.
Краткий экскурс в внедрение зависимостей или

Вступление

Данный небольшой обзор хочется посвятить такой штука, как CDI. Что это? CDI — это Contexts and Dependency Injection. Это спецификация Java EE, описывающая внедрение зависимостей (Dependency Injection) и контексты. Для информации можно посмотреть на сайт http://cdi-spec.org. Так как CDI — это спецификация (описание того, как оно должно работать, набор интерфейсов), то для использования нам понадобится и реализация. Одной из таких реализаций является Weld — http://weld.cdi-spec.org/ Для управления зависимостями и создания проекта воспользуемся Maven — https://maven.apache.org Итак, у нас установлен Maven, теперь будем разбираться сразу на практике, чтобы не разбираться в абстрактном. Для этого при помощи Maven создадим проект. Откроем командную строку (в Windows можно при помощи Win+R вызвать окно "Выполнить" и выполнить cmd) и попросим Maven всё сделать за нас. Для этого у Maven есть такое понятие, как архетип: Maven Archetype.
Краткий экскурс в внедрение зависимостей или
После этого на вопросах "Choose a number or apply filter" и "Choose org.apache.maven.archetypes:maven-archetype-quickstart version" просто нажимаем Enter. Далее вводим идентификаторы проекта, так называемые GAV (см. Naming Convention Guide).
Краткий экскурс в внедрение зависимостей или
После успешного создания проекта увидим надпись "BUILD SUCCESS". Теперь мы можем открывать наш проект в любимой IDE.

Добавление CDI в проект

Во вступлении мы увидели, что у CDI есть интересный сайт — http://www.cdi-spec.org/. Там есть раздел download, в котором есть таблица, которая содержит нужные нам данные:
Краткий экскурс в внедрение зависимостей или
Тут мы можем подсмотреть, как для Maven описывается тот факт, что мы используем в проекте API для CDI. API - это application programming interface, то есть некоторый программный интерфейс. Мы работаем с интерфейсом, не переживая о том, что и как за этим интерфейсом работает. API представляет из себя некоторый jar архив, который мы начнём использовать в своём проекте, то есть наш проект начинает зависеть от этого jar. Следовательно, CDI API для нашего проекта зависимость, dependency. В Maven проект описывается в файлах POM.xml (POM — Project Object Model). Зависимости описываются в блоке dependencies, в который нам и нужно добавить новую запись:

<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Как Вы могли заметить, мы не указываем scope со значением provided. Почему такое отличие? Такой scope означает, что нам зависимость предоставит кто-то. Когда приложение работает на Java EE сервере, то это означает что сервер предоставит приложению все необходимые JEE технологии. Мы же для простоты данного обзора будем работать в Java SE окружении, следовательно нам никто не предоставит данную зависимость. Подробнее про Dependency Scope можно прочитать тут: "Dependency Scope". Хорошо, у нас теперь есть возможность работать с интерфейсами. Но нам нужна и реализация. Как мы помним, мы будем использовать Weld. Интересно, что везде приводятся разные зависимости. Но мы будем следовать документации. Поэтому, прочитаем "18.4.5. Setting the Classpath" и сделаем как там сказано:

<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Важно, что версии Weld третьей линейки поддерживают CDI 2.0. Следовательно, мы можем рассчитывать на API этой версии. Теперь мы готовы к написанию кода.
Краткий экскурс в внедрение зависимостей или

Инициализация CDI контейнера

CDI — это механизм. Этим механизмом кто-то должен управлять. Как мы уже прочитали выше, то таким управляющим является контейнер. Следовательно, нам его нужно создать, сам он в SE окружении не появится. Допишем в наш main метод следующее:

public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Мы создали CDI контейнер вручную т.к. работаем в SE окружении. В обычных боевых проектах код выполняется на сервере, который предоставляет коду различные технологии. Соответственно, если сервер предоставляет CDI, то это значит, что на сервере уже есть CDI контейнер и нам не нужно будет ничего добавлять. Но для целей урока мы возьмём SE окружение. Кроме того, контейнер он вот, наглядно и понятно. Зачем нам контейнер? Контейнер внутри себя содержит бины (CDI beans).
Краткий экскурс в внедрение зависимостей или

CDI Beans

Итак, бины. Что такое CDI бин? Это Java класс, который соответствует некоторым правилам. Эти правила описаны в спецификации, в главе "2.2. What kinds of classes are beans?". Давайте добавим CDI бин в тот же пакет, где и класс App:

public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Теперь мы сможем вызвать этот бин из нашего main метода:

Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Как видно, мы не создавали бин при помощи ключевого слова new. Мы попросили у CDI контейнера: "CDI контейнер. Мне очень нужен экземпляр класса Logger, дай мне его пожалуйста". Такой способ называется "Dependency lookup", то есть поиск зависимости. А теперь давайте создадим новый класс:

public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Примитивный класс, возвращающий текстовое представление даты. Давайте теперь добавим вывод даты в сообщение:

public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
Появилась интересная аннотация @Inject. Как сказано в главе "4.1. Injection points" документации cdi weld, при помощи данной аннотации мы определяем Injection Point. На русском это можно прочитать как "точки внедрения". Они используются CDI контейнером, чтобы внедрять зависимости в момент инстанциирования бинов. Как видно, мы не присваиваем никаких значений полю dateSource (источник даты). Причиной тому тот факт, что CDI контейнер позволяет внутри CDI бинов (только те бины, которые он сам инстанциировал, т.е. которыми он управляет) использовать "Dependency Injection". Это другой способ Inversion of Control, подхода, когда зависимостью управляет кто-то другой, а не мы явно создаём объекты. Внедрение зависимостей может быть выполнено через метод, конструктор или поле. Подробнее см. главу спецификации CDI "5.5. Dependency injection". Процедура определения того, что нужно внедрять, называется typesafe resolution, о чём мы и должны поговорить.
Краткий экскурс в внедрение зависимостей или

Разрешение имени или Typesafe resolution

Обычно, в качестве типа внедряемого объекта используется интерфейс, а CDI контейнер сам определяет, какую реализацию нужно выбрать. Это полезно по многим причинам, о которых мы поговорим. Итак, у нас есть интерфейс логгера:

public interface Logger {
    void print(String message);
}
Он говорит, что если у нас есть некоторый логгер, мы можем передать ему сообщение и он выполнит свою задачу — залогирует. Как и куда — в данном случае интересовать не будет. Создадим теперь реализацию для логгера:

public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;
    
    public void print(String message) {
        System.out.println(message);
    }
}
Как видно, это логгер, который пишет в System.out. Прекрасно. Теперь, наш main метод отработает как и раньше. Logger logger = container.select(Logger.class).get(); Данная строка по прежнему получит логгер. И вся прелесть в том, что нам достаточно знать интерфейс, а о реализации уже думает за нас CDI контейнер. Допустим, у нас появляется вторая реализация, которая должна отправлять лог куда-то на удалённое хранилище:

public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Если сейчас запустить наш код без изменений, то мы получим ошибку, т.к. CDI контейнер видит у интерфейса две реализации и не может из них выбрать: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger Что же делать? Существует несколько доступных вариаций. Самый простой — аннотация @Vetoed нам CDI бином, чтобы CDI контейнер не воспринимал этот класс как CDI бин. Но есть куда более интересный подход. CDI бин может быть помечен как "альтернатива" при помощи аннотации @Alternative, описанной в главе "4.7. Alternatives" документации по Weld CDI. Что это значит? Это значит, что пока мы явно не скажем, что нужно использовать его, он не будет выбран. Это альтернативный вариант бина. Пометим бин NetworkLogger как @Alternative и мы увидим, что код снова выполняется и используется SystemOutLogger. Чтобы включить альтернативу у нас должен появиться файл beans.xml. Может возникнуть вопрос: "beans.xml, where do I put you?". Поэтому, разместим файл правильно:
Краткий экскурс в внедрение зависимостей или
Как только у нас появляется данный файл, то артефакт с нашим кодом будет называться "Explicit bean archive". Теперь у нас 2 отдельный конфигурации: программная и xml. Проблема в том, что они будут загружать одинаковые данные. Например, определение бина DataSource будет загружено 2 раза и при выполнении наша программа упадёт, т.к. CDI контейнер будет думать про них как про 2 отдельных бина (хотя по факту это один и тот же класс, о котором CDI контейнер узнал дважды). Чтобы это избежать есть 2 варианта:
  • убрать строку initializer.addPackages(App.class.getPackage()) и добавить указание альтернативы в xml файл:

<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • добавить в корневой элемент beans атрибут bean-discovery-mode со значением "none" и указать альтернативу программно:

initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Таким образом при помощи альтернативы CDI контейнер может определять, какой бин выбрать. Интересно, что если CDI контейнер будет знать несколько альтернатив для одного и того же интерфейса, то мы можем подсказать ему, указав приоритет при помощи аннотации @Priority (Начиная с CDI 1.1).
Краткий экскурс в внедрение зависимостей или

Квалификаторы

Отдельно стоит обсудить такую вещь как квалификаторы. Квалификатор указывается аннотацией над бином и уточняют поиск бина. А теперь подробнее. Интересно, что любой CDI бин в любом случае имеет как минимум один квалификатор — @Any. Если мы не указываем над бином НИ ОДИН квалификатор, но тогда CDI контейнер сам добавляет к квалификатору @Any ещё один квалификатор — @Default. Если же мы хоть что-то укажем (например, явно укажем @Any), то квалификатор @Default автоматически добавлен не будет. Но вся прелесть квалификаторов в том, что можно делать свои квалификаторы. Квалификатор почти ничем не отличается от аннотаций, т.к. по сути это и есть просто аннотация, написанная особым образом. Например, можно ввести Enum для типа протокола:

public enum ProtocolType {
    HTTP, HTTPS
}
Далее можем сделать квалификатор, который будет учитывать этот тип:

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Стоит отметить, что поля, помеченные как @Nonbinding не влияют на определение квалификатора. Теперь надо указать квалификатор. Указывается он над типом бина (чтобы CDI знал, как его определить) и над Injection Point (с аннотацией @Inject, чтобы понимать, какой бин искать для внедрения в этом месте). Например, мы можем добавить какой-нибудь класс с квалификатором. Для простоты для данной статьи сделаем их внутри NetworkLogger:

public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
И тогда когда будем выполнять Inject, то мы укажем квалификатор, который будет влиять на то, какой именно класс будет использован:

@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Здорово, не правда ли?) Кажется, что красиво, но непонятно зачем. А теперь представте следующее:

Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
Таким образом, мы можем переопределить получение значения value так, что оно может вычисляться динамически. Например, оно может браться из каких-нибудь настроек. Тогда мы можем менять реализацию даже на лету, без перекомпилирования или рестарта программы/сервера. Гораздо интереснее становится, не правда ли? )
Краткий экскурс в внедрение зависимостей или

Продюсеры

Ещё одной полезной возможностью CDI являются продюсеры. Это особые методы (они отмечены специальной аннотацией), которые вызываются, когда какой-то бин запросил внедрение зависимости. Подробнее описано в документации, в разделе "2.2.3. Producer methods". Самый простой пример:

@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Теперь при Inject'е в поля типа Integer будет вызван данный метод и из него будет получено значение. Тут стоит сразу понимать, что когда мы видим ключевое слово new, то надо сразу понимать, что это НЕ CDI бин. То есть экземпляр класса Random не станет CDI бином только потому, что он получен из чего-то, что контролирует CDI контейнер (в данном случае продюсер).
Краткий экскурс в внедрение зависимостей или

Interceptors

Интерцепторы — это такие перехватчики, "вклинивающиеся" в работу. В CDI это сделано довольно понятно. Давайте посмотрим, как мы можем сделать логирование при помощи интерпцепторов (или перехватчиков). Сначала, нам нужно описать привязку к интерцептору. Как и многое, это делается при помощи аннотаций:

@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
Тут главное, что это привязка для интерцептора (@InterceptorBinding), которая будет наследоваться при extends (@InterceptorBinding). Теперь напишем сам интерцептор:

@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Подробнее про то, как пишутся интерцепторы, можно прочитать в примере из спецификации: "1.3.6. Interceptor example". Ну а нам осталось только включить инерцептор. Для этого указываем аннотацию биндинга над выполняемым методом:

@ConsoleLog
public void print(String message) {
И теперь ещё очень важная деталь. Интерцепторы по умолчанию выключены и их надо включать по аналогии с альтернативами. Например, в файле beans.xml:

<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Как видите, довольно просто.
Краткий экскурс в внедрение зависимостей или

Event & Observers

CDI предосталвяет так же модель событий и наблюдателей. Тут не так всё очевидно, как с интерцепторами. Итак, Event'ом в данном случае может являться абсолютно любой класс, для описания ничего особого не надо. Например:

public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Теперь событие должен кто-то ожидать:

public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
Тут главное указать аннотацию @Observes, которая указывает, что это не просто метод, а метод, который должен быть вызван как результат наблюдения за событиями типа LogEvent. Ну и теперь нам нужен тот, кто будет наблюдать:

public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
У нас есть единственный метод, который будет говорить контейнеру, что случилось событие Event для типа события LogEvent. Теперь осталось только использовать наблюдатель. Например, в NetworkLogger мы можем добавить инжект нашего обсервера:

@Inject
private LogObserver observer;
А в методе print мы можем уведомлять наблюдателя о том, что у нас новое событие:

public void print(String message) {
	observer.observe(new LogEvent());
Тут важно знать, что события можно обрабатывать в одном потоке и в нескольких. Для асинхронной обработки служит метод .fireAsync (вместо .fire) и аннотация @ObservesAsync (вместо @Observes). Например, если все события выполняются в разных потоках, то если 1 поток упадёт с Exception, то остальные смогут выполнить свою работу для других событий. Подробнее про события в CDI можно прочитать, как обычно, в спецификации, в главе "10. Events".
Краткий экскурс в внедрение зависимостей или

Decorators

Как мы видели выше, под крылом CDI собраны различные паттерны проектирования. И вот ещё один - декоратор. Это очень интересная штука. Давайте взглянем на такой вот класс:

@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
Объявляя его декоратором, мы говорим, что когда будет исопльзована какая либо реализация Logger то будет использоваться эта "надстройка", которая знает настоящую реализацию, которая хранится в поле delegate (т.к. оно помечено аннотацией @Delegate). Декораторы могут быть ассоциированы только с CDI бином, который сам не интерцептор и не декоратор. Пример можно увидеть так же в спецификации: "1.3.7. Decorator example". Декоратор, как и интерцептор, надо включать. Например, в beans.xml:

<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Подробнее см. weld reference: "Chapter 10. Decorators".

Жизненный цикл

У бинов есть свой жизненный цикл. Выглядит он примерно так:
Краткий экскурс в внедрение зависимостей или
Как видно по картинке, у нас есть так называемые lifecycle callbacks. Это аннотации, которые скажут CDI контейнеру вызывать определённые методы на определённом этапе жизненного цикла бина. Например:

@PostConstruct
public void init() {
	System.out.println("Inited");
}
Такой метод будет вызывать при инстанциировании бина CDI контейнером. Аналогично будет и с @PreDestroy при уничтожении бина, когда он станет не нужен. В аббревиатуре CDI не зря есть буква C - Context. Бины в CDI являются contextual, то есть их жизненный цикл зависит от контекста, в котором они существуют внутри CDI контейнера. Чтобы в этом лучше разбираться стоит прочитать раздел спецификиации "7. Lifecycle of contextual instances". Так же стоит знать, что есть жизненный цикл и у самого контейнера, о чём можно прочитать в "Container lifecycle events".
Краткий экскурс в внедрение зависимостей или

Итого

Выше мы рассмотрели самую верхушку айсберга под названием CDI. CDI является частью JEE спецификации и используется в JavaEE окружении. Те, кто используют Spring используют не CDI, а DI, то есть это несколько разные спецификации. Но зная и понимаю вышеуказанное легко можно перестроиться. Учитывая, что Spring поддерживает аннотации из мира CDI (те же Inject). Дополнительные материалы: #Viacheslav
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
rmuskovets Уровень 41
22 января 2019
А ти же 3 лвл! Слишком много знаем, не кажется?