JavaRush /Java блог /Random UA /Короткий екскурс у впровадження залежностей чи "Що ще за ...
Viacheslav
3 рівень

Короткий екскурс у впровадження залежностей чи "Що ще за CDI?"

Стаття з групи Random UA
Основою, де зараз побудовані найпопулярніші фреймворки, є використання залежностей. Пропоную подивитися, що про це йдеться у специфікації 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 для нашого проекту залежить залежність. У 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.codegym.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);
Таким чином, ми можемо перевизначити отримання значення значення так, що воно може обчислюватися динамічно. Наприклад, воно може братися з якихось налаштувань. Тоді ми можемо змінювати реалізацію навіть на льоту, без перекомпілювання чи рестарту програми/сервера. Набагато цікавіше стає, чи не так? )
Короткий екскурс у впровадження залежностей або

Продюсери

Ще однією корисною можливістю 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.codegym.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.codegym.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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ