Перейти к основному содержимому

Введение в CDI (внедрение контекстов и зависимостей) в Java

· 9 мин. чтения

1. Обзор

CDI (Contexts and Dependency Injection) — это стандартная среда внедрения зависимостей , включенная в Java EE 6 и выше.

Это позволяет нам управлять жизненным циклом компонентов с состоянием через контексты жизненного цикла, специфичные для предметной области, и внедрять компоненты (службы) в клиентские объекты безопасным для типов способом.

В этом руководстве мы подробно рассмотрим наиболее важные функции CDI и реализуем различные подходы для внедрения зависимостей в клиентские классы.

2. DYDI (внедрение зависимостей своими руками)

Короче говоря, можно реализовать DI, вообще не прибегая к какой-либо структуре.

Этот подход широко известен как DYDI (внедрение зависимостей «сделай сам»).

С помощью DYDI мы изолируем код приложения от создания объекта, передавая необходимые зависимости в клиентские классы через старые простые фабрики/сборщики.

Вот как может выглядеть базовая реализация DYDI:

public interface TextService {
String doSomethingWithText(String text);
String doSomethingElseWithText(String text);
}
public class SpecializedTextService implements TextService { ... }
public class TextClass {
private TextService textService;

// constructor
}
public class TextClassFactory {

public TextClass getTextClass() {
return new TextClass(new SpecializedTextService();
}
}

Конечно, DYDI подходит для некоторых относительно простых случаев использования.

Если бы наше примерное приложение увеличилось в размерах и усложнилось, реализовав более крупную сеть взаимосвязанных объектов, мы бы в конечном итоге засорили его тоннами фабрик объектных графов.

Это потребовало бы большого количества шаблонного кода только для создания графов объектов. Это не полностью масштабируемое решение.

Можем ли мы сделать DI лучше? Конечно можем. Вот здесь-то и появляется CDI.

3. Простой пример

CDI превращает DI в простой процесс, сводящийся к простому украшению классов обслуживания несколькими простыми аннотациями и определению соответствующих точек внедрения в клиентских классах.

Чтобы продемонстрировать, как CDI реализует DI на самом базовом уровне, давайте предположим, что мы хотим разработать простое приложение для редактирования файлов изображений. Возможность открытия, редактирования, записи, сохранения файла изображения и так далее.

3.1. Файл «beans.xml »

Во- первых, мы должны поместить файл «beans.xml» в папку «src/main/resources/META-INF/» . Даже если этот файл вообще не содержит каких-либо конкретных директив DI, он необходим для запуска и запуска CDI :

<beans xmlns="http://java.sun.com/xml/ns/javaee" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

3.2. Классы обслуживания

Далее давайте создадим сервисные классы, которые выполняют упомянутые выше файловые операции с файлами GIF, JPG и PNG:

public interface ImageFileEditor {
String openFile(String fileName);
String editFile(String fileName);
String writeFile(String fileName);
String saveFile(String fileName);
}
public class GifFileEditor implements ImageFileEditor {

@Override
public String openFile(String fileName) {
return "Opening GIF file " + fileName;
}

@Override
public String editFile(String fileName) {
return "Editing GIF file " + fileName;
}

@Override
public String writeFile(String fileName) {
return "Writing GIF file " + fileName;
}

@Override
public String saveFile(String fileName) {
return "Saving GIF file " + fileName;
}
}
public class JpgFileEditor implements ImageFileEditor {
// JPG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
...
}
public class PngFileEditor implements ImageFileEditor {
// PNG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
...
}

3.3. Клиентский класс

Наконец, давайте реализуем клиентский класс, который принимает реализацию ImageFileEditor в конструкторе, и определим точку внедрения с аннотацией @Inject :

public class ImageFileProcessor {

private ImageFileEditor imageFileEditor;

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor) {
this.imageFileEditor = imageFileEditor;
}
}

Проще говоря, аннотация @Inject — настоящая рабочая лошадка CDI. Это позволяет нам определять точки внедрения в клиентских классах.

В этом случае @Inject указывает CDI внедрить реализацию ImageFileEditor в конструктор.

Кроме того, также возможно внедрить службу, используя аннотацию @Inject в полях (внедрение поля) и сеттерах (внедрение сеттера). Мы рассмотрим эти варианты позже.

3.4. Построение графа объекта ImageFileProcessor с помощью Weld

Конечно, нам нужно убедиться, что CDI внедрит правильную реализацию ImageFileEditor в конструктор класса ImageFileProcessor .

Для этого, во-первых, мы должны получить экземпляр класса.

Поскольку мы не будем полагаться на какой-либо сервер приложений Java EE для использования CDI, мы сделаем это с Weld , эталонной реализацией CDI в Java SE :

public static void main(String[] args) {
Weld weld = new Weld();
WeldContainer container = weld.initialize();
ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get();

System.out.println(imageFileProcessor.openFile("file1.png"));

container.shutdown();
}

Здесь мы создаем объект WeldContainer , затем получаем объект ImageFileProcessor и, наконец, вызываем его метод openFile() .

Как и ожидалось, если мы запустим приложение, CDI будет громко жаловаться, выбрасывая исключение DeploymentException:

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...

Мы получаем это исключение, потому что CDI не знает, какую реализацию ImageFileEditor следует внедрить в конструктор ImageFileProcessor .

В терминологии CDI это известно как исключение неоднозначной инъекции .

3.5. Аннотации @Default и @Alternative _

Разрешить эту двусмысленность несложно. CDI по умолчанию аннотирует все реализации интерфейса аннотацией @Default .

Итак, мы должны явно указать, какую реализацию следует внедрить в клиентский класс:

@Alternative
public class GifFileEditor implements ImageFileEditor { ... }
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }
public class PngFileEditor implements ImageFileEditor { ... }

В этом случае мы аннотировали GifFileEditor и JpgFileEditor аннотацией @Alternative , поэтому CDI теперь знает, что PngFileEditor (по умолчанию аннотированный аннотацией @Default ) является реализацией, которую мы хотим внедрить.

Если мы перезапустим приложение, на этот раз оно будет выполнено, как и ожидалось:

Opening PNG file file1.png

Кроме того, аннотирование PngFileEditor аннотацией @Default и сохранение других реализаций в качестве альтернатив приведет к тому же результату, что и выше.

Вкратце это показывает, как мы можем очень легко поменять местами внедрение реализаций во время выполнения, просто переключив аннотации @Alternative в классах обслуживания .

4. Полевая инъекция

CDI поддерживает как полевые, так и сеттерные инъекции из коробки.

Вот как выполнить внедрение полей ( правила квалификации сервисов с аннотациями @Default и @Alternative остаются прежними ):

@Inject
private final ImageFileEditor imageFileEditor;

5. Инъекция сеттера

Точно так же вот как сделать инъекцию сеттера:

@Inject 
public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. Аннотация @Named

До сих пор мы научились определять точки внедрения в клиентских классах и внедрять сервисы с помощью аннотаций @Inject , @Default и @Alternative , которые охватывают большинство вариантов использования.

Тем не менее, CDI также позволяет нам выполнять сервисную инъекцию с аннотацией @Named .

Этот метод обеспечивает более семантический способ внедрения сервисов, привязывая осмысленное имя к реализации:

@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

@Named("JpgFileEditor")
public class JpgFileEditor implements ImageFileEditor { ... }

@Named("PngFileEditor")
public class PngFileEditor implements ImageFileEditor { ... }

Теперь мы должны реорганизовать точку внедрения в классе ImageFileProcessor , чтобы она соответствовала именованной реализации:

@Inject 
public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

Также можно выполнять внедрение полей и сеттеров с именованными реализациями, что очень похоже на использование аннотаций @Default и @Alternative :

@Inject 
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

@Inject
public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. Аннотация @Produces

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

CDI обеспечивает поддержку таких ситуаций с помощью аннотации @Produces .

@Produces позволяет нам реализовывать фабричные классы, в обязанности которых входит создание полностью инициализированных сервисов.

Чтобы понять, как работает аннотация @Produces , давайте реорганизуем класс ImageFileProcessor , чтобы он мог принимать в конструкторе дополнительную службу TimeLogger .

Сервис будет использоваться для регистрации времени выполнения определенной операции с файлом изображения:

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

public String openFile(String fileName) {
return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime();
}

// additional image file methods

В этом случае класс TimeLogger использует две дополнительные службы, SimpleDateFormat и Calendar :

public class TimeLogger {

private SimpleDateFormat dateFormat;
private Calendar calendar;

// constructors

public String getTime() {
return dateFormat.format(calendar.getTime());
}
}

Как сообщить CDI, где искать полностью инициализированный объект TimeLogger ?

Мы просто создаем фабричный класс TimeLogger и аннотируем его фабричный метод аннотацией @Produces :

public class TimeLoggerFactory {

@Produces
public TimeLogger getTimeLogger() {
return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
}
}

Всякий раз, когда мы получаем экземпляр ImageFileProcessor , CDI сканирует класс TimeLoggerFactory , затем вызывает метод getTimeLogger() (поскольку он помечен аннотацией @Produces ) и, наконец, внедряет службу Time Logger .

Если мы запустим рефакторинг примера приложения с Weld , он выведет следующее:

Opening PNG file file1.png at: 17:46

8. Пользовательские квалификаторы

CDI поддерживает использование настраиваемых квалификаторов для определения зависимостей и устранения неоднозначных точек внедрения.

Пользовательские квалификаторы — очень мощная функция. Они не только связывают семантическое имя со службой, но также связывают метаданные внедрения. Метаданные, такие как RetentionPolicy и цели юридических аннотаций ( ElementType ).

Давайте посмотрим, как использовать пользовательские квалификаторы в нашем приложении:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface GifFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface JpgFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PngFileEditorQualifier {}

Теперь давайте свяжем пользовательские квалификаторы с реализациями ImageFileEditor :

@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

Наконец, давайте рефакторим точку внедрения в классе ImageFileProcessor :

@Inject
public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

Если мы запустим наше приложение еще раз, оно должно сгенерировать тот же вывод, что и выше.

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

Кроме того, пользовательские квалификаторы позволяют нам определять более строгие точки внедрения типов (превосходя функциональность аннотаций @Default и @Alternative) .

Если в иерархии типов определен только подтип, то CDI будет внедрять только подтип, а не базовый тип.

9. Заключение

Несомненно, CDI делает внедрение зависимостей легкой задачей , затраты на дополнительные аннотации — очень небольшие усилия для получения организованного внедрения зависимостей.

Бывают случаи, когда DYDI все еще имеет свое место над CDI. Например, при разработке довольно простых приложений, содержащих только простые графы объектов.

Как всегда, все примеры кода, показанные в этой статье, доступны на GitHub .