JavaRush /Java Blog /Random-TL /Isang maikling iskursiyon sa dependency injection o "Ano ...

Isang maikling iskursiyon sa dependency injection o "Ano pa ang CDI?"

Nai-publish sa grupo
Ang pundasyon kung saan itinayo ngayon ang pinakasikat na mga framework ay ang dependency injection. Iminumungkahi kong tingnan kung ano ang sinasabi ng detalye ng CDI tungkol dito, kung anong mga pangunahing kakayahan ang mayroon tayo at kung paano natin magagamit ang mga ito.
Краткий экскурс в внедрение зависимостей or

Panimula

Nais kong italaga ang maikling pagsusuri na ito sa isang bagay tulad ng CDI. Ano ito? Ang ibig sabihin ng CDI ay Contexts and Dependency Injection. Isa itong espesipikasyon ng Java EE na naglalarawan ng Dependency Injection at mga konteksto. Para sa impormasyon, maaari mong tingnan ang website na http://cdi-spec.org . Dahil ang CDI ay isang detalye (isang paglalarawan kung paano ito dapat gumana, isang hanay ng mga interface), kakailanganin din namin ng pagpapatupad upang magamit ito. Isa sa mga naturang pagpapatupad ay Weld - http://weld.cdi-spec.org/ Upang pamahalaan ang mga dependency at lumikha ng isang proyekto, gagamitin namin ang Maven - https://maven.apache.org Kaya, na-install namin ang Maven, ngayon kami ay mauunawaan ito sa pagsasanay, upang hindi maunawaan ang abstract. Para magawa ito, gagawa kami ng proyekto gamit ang Maven. Buksan natin ang command line (sa Windows, maaari mong gamitin ang Win+R para buksan ang "Run" window at i-execute ang cmd) at hilingin kay Maven na gawin ang lahat para sa amin. Para dito, may konsepto si Maven na tinatawag na archetype: Maven Archetype .
Краткий экскурс в внедрение зависимостей or
Pagkatapos nito, sa mga tanong na “ Pumili ng numero o ilapat ang filter ” at “ Pumili ng org.apache.maven.archetypes:maven-archetype-quickstart na bersyon ” pindutin lang ang Enter. Susunod, ilagay ang mga identifier ng proyekto, ang tinatawag na GAV (tingnan ang Naming Convention Guide ).
Краткий экскурс в внедрение зависимостей or
Matapos ang matagumpay na paglikha ng proyekto, makikita natin ang inskripsiyon na "BUILD SUCCESS". Ngayon ay maaari na naming buksan ang aming proyekto sa aming paboritong IDE.

Pagdaragdag ng CDI sa isang Proyekto

Sa panimula, nakita namin na ang CDI ay may isang kawili-wiling website - http://www.cdi-spec.org/ . Mayroong seksyon ng pag-download, na naglalaman ng talahanayan na naglalaman ng data na kailangan namin:
Краткий экскурс в внедрение зависимостей or
Dito natin makikita kung paano inilarawan ni Maven ang katotohanang ginagamit natin ang CDI API sa proyekto. Ang API ay isang application programming interface, iyon ay, ilang programming interface. Nagtatrabaho kami sa interface nang hindi nababahala tungkol sa kung ano at paano ito gumagana sa likod ng interface na ito. Ang API ay isang jar archive na sisimulan naming gamitin sa aming proyekto, ibig sabihin, ang aming proyekto ay nagsisimulang umasa sa garapon na ito. Samakatuwid, ang CDI API para sa aming proyekto ay isang dependency. Sa Maven, inilalarawan ang isang proyekto sa mga POM.xml file ( POM - Project Object Model ). Ang mga dependencies ay inilarawan sa block ng dependencies, kung saan kailangan nating magdagdag ng bagong entry:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Как Вы могли заметить, мы не указываем scope со meaningм provided. Почему такое отличие? Такой scope означает, что нам зависимость предоставит кто-то. Когда приложение работает на Java EE serverе, то это означает что server предоставит приложению все необходимые JEE технологии. Мы же для простоты данного обзора будем работать в Java SE окружении, следовательно нам никто не предоставит данную зависимость. Подробнее про Dependency Scope можно прочитать тут: "Dependency Scope". Хорошо, у нас теперь есть возможность работать с интерфейсами. Но нам нужна и реализация. Как мы помним, мы будем использовать Weld. Интересно, что везде приводятся разные зависимости. Но мы будем следовать documentации. Поэтому, прочитаем "18.4.5. Setting the Classpath" и сделаем How там сказано:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Важно, что версии Weld третьей линейки поддерживают CDI 2.0. Следовательно, мы можем рассчитывать на API этой версии. Теперь мы готовы к написанию codeа.
Краткий экскурс в внедрение зависимостей or

Инициализация 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 окружении. В обычных боевых проектах code выполняется на serverе, который предоставляет codeу различные технологии. Соответственно, если server предоставляет CDI, то это значит, что на serverе уже есть CDI контейнер и нам не нужно будет ничего добавлять. Но для целей урока мы возьмём SE окружение. Кроме того, контейнер он вот, наглядно и понятно. Зачем нам контейнер? Контейнер внутри себя содержит бины (CDI beans).
Краткий экскурс в внедрение зависимостей or

CDI Beans

Итак, бины. What такое 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. Мы попросor у 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);
    }
}
Появилась интересная annotation @Inject. Как сказано в главе "4.1. Injection points" documentации cdi weld, при помощи данной аннотации мы определяем Injection Point. На русском это можно прочитать How "точки внедрения". Они используются CDI контейнером, чтобы внедрять зависимости в момент инстанциирования бинов. Как видно, мы не присваиваем ниHowих значений полю dateSource (источник даты). Причиной тому тот факт, что CDI контейнер позволяет внутри CDI бинов (только те бины, которые он сам инстанциировал, т.е. которыми он управляет) использовать "Dependency Injection". Это другой способ Inversion of Control, подхода, когда зависимостью управляет кто-то другой, а не мы явно создаём an objectы. Внедрение зависимостей может быть выполнено через метод, конструктор or поле. Подробнее см. главу спецификации CDI "5.5. Dependency injection". Процедура определения того, что нужно внедрять, называется typesafe resolution, о чём мы и должны поговорить.
Краткий экскурс в внедрение зависимостей or

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

Обычно, в качестве типа внедряемого an object используется интерфейс, а CDI контейнер сам определяет, Howую реализацию нужно выбрать. Это полезно по многим причинам, о которых мы поговорим. Итак, у нас есть интерфейс логгера:
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 метод отработает How и раньше. Logger logger = container.select(Logger.class).get(); Данная строка по прежнему получит логгер. И вся прелесть в том, что нам достаточно знать интерфейс, а о реализации уже думает за нас CDI контейнер. Допустим, у нас появляется вторая реализация, которая должна отправлять лог куда-то на удалённое хранorще:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Если сейчас запустить наш code без изменений, то мы получим ошибку, т.к. CDI контейнер видит у интерфейса две реализации и не может из них выбрать: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger What же делать? Существует несколько доступных вариаций. Самый простой — annotation @Vetoed нам CDI бином, чтобы CDI контейнер не воспринимал этот класс How CDI бин. Но есть куда более интересный подход. CDI бин может быть помечен How "альтернатива" при помощи аннотации @Alternative, описанной в главе "4.7. Alternatives" documentации по Weld CDI. What это значит? Это значит, что пока мы явно не скажем, что нужно использовать его, он не будет выбран. Это альтернативный вариант бина. Пометим бин NetworkLogger How @Alternative и мы увидим, что code снова выполняется и используется SystemOutLogger. Whatбы включить альтернативу у нас должен появиться файл beans.xml. Может возникнуть вопрос: "beans.xml, where do I put you?". Поэтому, разместим файл правильно:
Краткий экскурс в внедрение зависимостей or
Как только у нас появляется данный файл, то артефакт с нашим codeом будет называться "Explicit bean archive". Теперь у нас 2 отдельный конфигурации: программная и xml. Проблема в том, что они будут загружать одинаковые данные. Например, определение бина DataSource будет загружено 2 раза и при выполнении наша программа упадёт, т.к. CDI контейнер будет думать про них How про 2 отдельных бина (хотя по факту это один и тот же класс, о котором CDI контейнер узнал дважды). Whatбы это избежать есть 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 со meaningм "none" и указать альтернативу программно:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Таким образом при помощи альтернативы CDI контейнер может определять, Howой бин выбрать. Интересно, что если CDI контейнер будет знать несколько альтернатив для одного и того же интерфейса, то мы можем подсказать ему, указав приоритет при помощи аннотации @Priority (Начиная с CDI 1.1).
Краткий экскурс в внедрение зависимостей or

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

Отдельно стоит обсудить такую вещь How квалификаторы. Квалификатор указывается аннотацией над бином и уточняют поиск бина. А теперь подробнее. Интересно, что любой CDI бин в любом случае имеет How минимум один квалификатор — @Any. Если мы не указываем над бином НИ ОДИН квалификатор, но тогда CDI контейнер сам добавляет к квалификатору @Any ещё один квалификатор — @Default. Если же мы хоть что-то укажем (например, явно укажем @Any), то квалификатор @Default автоматически добавлен не будет. Но вся прелесть квалификаторов в том, что можно делать свои квалификаторы. Квалификатор почти ничем не отличается от аннотаций, т.к. по сути это и есть просто annotation, написанная особым образом. Например, можно ввести Enum для типа протокола:
public enum ProtocolType {
    HTTP, HTTPS
}
Далее можем сделать квалификатор, который будет учитывать этот тип:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Стоит отметить, что поля, помеченные How @Nonbinding не влияют на определение квалификатора. Теперь надо указать квалификатор. Указывается он над типом бина (чтобы CDI знал, How его определить) и над Injection Point (с аннотацией @Inject, чтобы понимать, Howой бин искать для внедрения в этом месте). Например, мы можем добавить Howой-нибудь класс с квалификатором. Для простоты для данной статьи сделаем их внутри 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, то мы укажем квалификатор, который будет влиять на то, Howой именно класс будет использован:
@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 так, что оно может вычисляться динамически. Например, оно может браться из Howих-нибудь настроек. Тогда мы можем менять реализацию даже на лету, без перекомпorрования or рестарта программы/serverа. Гораздо интереснее становится, не правда ли? )
Краткий экскурс в внедрение зависимостей or

Продюсеры

Ещё одной полезной возможностью CDI являются продюсеры. Это особые методы (они отмечены специальной аннотацией), которые вызываются, когда Howой-то бин requestил внедрение зависимости. Подробнее описано в documentации, в разделе "2.2.3. Producer methods". Самый простой пример:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Теперь при Inject'е в поля типа Integer будет вызван данный метод и из него будет получено meaning. Тут стоит сразу понимать, что когда мы видим ключевое слово new, то надо сразу понимать, что это НЕ CDI бин. То есть экземпляр класса Random не станет CDI бином только потому, что он получен из чего-то, что контролирует CDI контейнер (в данном случае продюсер).
Краткий экскурс в внедрение зависимостей or

Interceptors

Интерцепторы — это такие перехватчики, "вклинивающиеся" в работу. В CDI это сделано довольно понятно. Давайте посмотрим, How мы можем сделать логирование при помощи интерпцепторов (or перехватчиков). Сначала, нам нужно описать привязку к интерцептору. Как и многое, это делается при помощи аннотаций:
@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();
    }
}
Подробнее про то, How пишутся интерцепторы, можно прочитать в примере из спецификации: "1.3.6. Interceptor example". Ну а нам осталось только включить инерцептор. Для этого указываем аннотацию биндинга над выполняемым методом:
@ConsoleLog
public void print(String message) {
И теперь ещё очень важная деталь. Интерцепторы по умолчанию выключены и их надо включать по аналогии с альтернативами. Например, в файле beans.xml:
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Как видите, довольно просто.
Краткий экскурс в внедрение зависимостей or

Event & Observers

CDI предосталвяет так же модель событий и наблюдателей. Тут не так всё очевидно, How с интерцепторами. Итак, 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, которая указывает, что это не просто метод, а метод, который должен быть вызван How результат наблюдения за событиями типа LogEvent. Ну и теперь нам нужен тот, кто будет наблюдать:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
У нас есть единственный метод, который будет говорить контейнеру, что случилось событие Event для типа события LogEvent. Теперь осталось только использовать наблюдатель. Например, в NetworkLogger мы можем добавить инжект нашего обserverа:
@Inject
private LogObserver observer;
А в методе print мы можем уведомлять наблюдателя о том, что у нас новое событие:
public void print(String message) {
	observer.observe(new LogEvent());
Тут важно знать, что события можно обрабатывать в одном потоке и в нескольких. Для асинхронной обработки служит метод .fireAsync (instead of .fire) и annotation @ObservesAsync (instead of @Observes). Например, если все события выполняются в разных потоках, то если 1 поток упадёт с Exception, то остальные смогут выполнить свою работу для других событий. Подробнее про события в CDI можно прочитать, How обычно, в спецификации, в главе "10. Events".
Краткий экскурс в внедрение зависимостей or

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);
    }
}
Объявляя его декоратором, мы говорим, что когда будет исопльзована Howая либо реализация Logger то будет использоваться эта "надстройка", которая знает настоящую реализацию, которая хранится в поле delegate (т.к. оно помечено аннотацией @Delegate). Декораторы могут быть ассоциированы только с CDI бином, который сам не интерцептор и не декоратор. Пример можно увидеть так же в спецификации: "1.3.7. Decorator example". Декоратор, How и интерцептор, надо включать. Например, в beans.xml:
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Подробнее см. weld reference: "Chapter 10. Decorators".

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

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

Total

Выше мы рассмотрели самую верхушку айсберга под названием CDI. CDI является частью JEE спецификации и используется в JavaEE окружении. Те, кто используют Spring используют не CDI, а DI, то есть это несколько разные спецификации. Но зная и понимаю вышеуказанное легко можно перестроиться. Учитывая, что Spring поддерживает аннотации из мира CDI (те же Inject). Дополнительные материалы: #Viacheslav
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION