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

1. Обзор

CDI (внедрение контекстов и зависимостей) - это стандартная среда dependency внедрение , включенная в 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 поддерживает использование пользовательских квалификаторов для определения зависимостей и решения неоднозначных точек внедрения.

Пользовательские квалификаторы - очень мощная функция. Они не только связывают семантическое имя со службой, но и с метаданными внедрения. Метаданные, такие как https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/RetentionPolicy . html[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. Как и при разработке довольно простых приложений, которые содержат только простые графы объектов.

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