Статьи

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

CDI (контексты и внедрение зависимостей) — это спецификация внедрения зависимостей (DI), связанная с Java EE 6 и выше. Он реализует основанную на аннотациях структуру DI, облегчая полную автоматизацию управления зависимостями и позволяя разработчикам сосредоточиться на кодировании, а не на работе с DI во всех его различных направлениях.

DI является одной из наиболее распространенных форм инверсии управления, используемых в объектно-ориентированном проектировании . Но это не означает, что реализация шаблона вручную по своей природе проста, поскольку всегда требует явной реализации какого-либо творческого шаблона, такого как фабрики или сборщики, в качестве базовых инжекторов.

По общему признанию, несколько популярных фреймворков, включая Google Guice и даже Spring , сделают построение графов объектов намного менее болезненным. CDI стандартизирован, тем не менее, давая вам выбор из нескольких реализаций. И борьба с бременем полноценной платформы, такой как Spring, не всегда лучший подход, если вы просто хотите сделать простой DI. CDI, с другой стороны, нацелена на то, чтобы сделать DI легким делом, не прибегая к каким-либо внешним системам.

Прежде всего, для его работы требуется работающая реализация. Есть несколько доступных прямо сейчас, в том числе OpenWebBeans , CauchoCanDI и Weld . Последняя является эталонной реализацией, которая может использоваться в различных контекстах и ​​средах, включая Java SE.

И последнее, но не менее важное: нет никаких ограничений для внедрения зависимостей, функция, которая позволяет быстро вставлять ресурсы Java EE, такие как менеджеры сущностей JPA . Другие структуры (яркий пример этого — Guice) также обеспечивают поддержку внедрения JPA / менеджера сущностей , но процесс довольно громоздкий и не полностью стандартизирован. Это открывает двери для использования стандарта в приложениях, управляемых базой данных, и эту тему я планирую подробно рассмотреть в дальнейшем.

С учетом сказанного в этом руководстве я предоставлю вам практическое руководство по использованию CDI / Weld в Java SE.

Простое введение зависимости с помощью CDI

Сам по себе DI становится необходимостью, когда речь идет о разработке разобщенных компонентов на основе нескольких полиморфных реализаций. Типичный вариант использования имеет отдельный контракт , определенный через интерфейс или абстрактный класс, и несколько реализаций, где одна или несколько из них должны быть внедрены в объект клиента во время выполнения.

Создание точки впрыска

Рассмотрим следующий пример, который является частью простого приложения, которое обрабатывает данную строку несколькими основными способами:

public interface TextProcessor { String processText(String text); } 

API для обработки строк настолько прост, что не заслуживает дальнейшего анализа. Итак, вот соответствующие реализации:

 public class LowerCaseTextProcessor implements TextProcessor { public String processText(String text) { return text.toLowerCase(); } } public class UppercaseTextProcessor implements TextProcessor { public String processText(String text) { return text.toUpperCase(); } } public class ReverseTextProcessor implements TextProcessor { public String processText(String text) { return new StringBuilder(text).reverse().toString(); } } 

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

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

 public class TextApplication { private TextProcessor textProcessor; private BufferedReader userInputReader; @Inject public TextApplication(TextProcessor textProcessor) { this.textProcessor = textProcessor; this.userInputReader = new BufferedReader(new InputStreamReader(System.in)); } public void run() throws IOException { System.out.println("Enter the text to process : "); String text = userInputReader.readLine(); System.out.println(textProcessor.processText(text)); } } 

Помимо злого оператора new я использовал для создания объекта BufferedReader (потерпите меня, пока я буду его реорганизовывать позже), первое, на что стоит обратить внимание, это использование аннотации @Inject , которая сообщает CDI, что Экземпляр интерфейса TextProcessor должен быть TextProcessor в конструктор класса. Это инъекция конструктора ванили легко!

Bootstrapping Weld

Ну, не все так просто. Сначала нам нужно скачать артефакт Weld — вот он для Maven:

 <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se</artifactId> <version>2.4.0.Final</version> </dependency> 

При наличии артефакта Weld мы должны создать файл beans.xml каталоге src/main/java/resources/META-INF/ , поскольку CDI должен сканировать этот файл, даже если он не содержит дополнительных параметров внедрения. Вот как может выглядеть типичная версия этого файла:

 <?xml version="1.0" encoding="UTF-8"?> <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" bean-discovery-mode="all"> </beans> 

Затем мы должны программно загрузить Weld , TextApplication класса TextApplication из контейнера Weld и, наконец, начать использовать его по желанию, как TextApplication ниже:

 public class Main { public static void main(String[] args) throws IOException { Weld weld = new Weld(); WeldContainer container = weld.initialize(); TextApplication textApplication = container.instance().select(TextApplication.class).get(); textApplication.run(); weld.shutdown(); } } 

Все должно работать как положено. Или не должно?

@Default реализации с @Default и @Alternative

Когда вы запускаете приложение, CDI выдает уродливое DeploymentException с сообщением «Неудовлетворенные зависимости для типа TextProcessor с квалификаторами @Default». Короче говоря, CDI просто говорит вам, что не знает, какую реализацию внедрить в конструктор класса TextApplication .

Как нам сообщить CDI, какую реализацию выбрать во время выполнения? Что ж, первый и самый элементарный подход — это использование аннотаций @Default и @Alternative и указание CDI вводить конкретную реализацию TextProcessor .

По умолчанию CDI назначает аннотацию @Default всем потенциально внедряемым объектам. Итак, если мы хотим внедрить реализацию UppercaseTextProcessor , разработчики должны быть аннотированы следующим образом:

 @Default public class UppercaseTextProcessor implements TextProcessor { ... } @Alternative public class LowercaseTextProcessor implements TextProcessor { ... } @Alternative public class ReverseTextProcessor implements TextProcessor { ... } 

После изменения классов запустите приложение еще раз. Вам будет предложено ввести строку в консоли, и, как и следовало ожидать, вывод будет выглядеть красиво в верхнем регистре! Посмотрите, насколько легко использовать CDI / Weld и насколько они мощны, даже если вы делаете что-то столь же тривиальное, как разбор строки?

Однако, если вы достаточно @Alternative , вам будет интересно, какой смысл в том, чтобы несколько разработчиков интерфейсов аннотировались @Alternative , если только тот, который помечен @Default будет введен? Альтернативы — это отличная функция, которая позволяет вам выбирать реализацию во время запуска, а не во время компиляции, как мы делали выше. Чтобы сделать это, аннотируйте всех @Alternative аннотацией @Alternative и используйте обязательный файл beans.xml чтобы указать, какую реализацию следует внедрить в данный клиентский объект:

 <beans> <alternatives> <!-- in a real system the class names need to be fully qualified --> <class>UppercaseTextProcessor</class> <!-- <class>LowercaseTextProcessor</class> --> <!-- <class>ReverseTextProcessor</class> --> </alternatives> </beans> 

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

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

Чтобы решить эту проблему, CDI предоставляет несколько более интуитивно понятных механизмов для достижения того же результата, включая аннотацию @Named и пользовательские квалификаторы.

Представляем аннотацию @Named

На самом деле, использование аннотации @Named не отличается от работы с @Default и @Alternative . Его основное преимущество заключается в возможности привязки более семантического и осмысленного имени к конкретной реализации. Кроме того, многие IDE подскажут вам, какую зависимость вы хотите внедрить.

Рассмотрим переработанные версии предыдущих реализаций:

 @Named("Uppercase") public class UppercaseTextProcessor implements TextProcessor { ... } @Named("Lowercase") public class LowercaseTextProcessor implements TextProcessor { ... } @Named("Reverse") public class ReverseTextProcessor implements TextProcessor { ... } 

Теперь, если мы хотим внедрить экземпляр класса UppercaseTextProcessor , точка внедрения должна быть рефакторизована следующим образом:

 @Inject public TextApplication( @Named("Uppercase") TextProcessor textProcessor) { ... } 

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

Более того, CDI не будет ограничивать вас простым внедрением простого конструктора, так как внедрение полей и сеттера также поддерживается аккуратно.

Использование полевых и сеттерных инъекций

Даже если среди опытных разработчиков существует общее мнение о том, что внедрение конструктора считается наиболее эффективным способом выполнения DI, поскольку оно гарантирует, что объект всегда инициализируется в допустимом состоянии, CDI позволит вам безболезненно выполнять инъекцию поля и сеттера. ,

Вот как должно быть сделано первое, когда TextProcessor интерфейса @Default @Alternative аннотации @Default и @Alternative :

 @Inject private TextProcessor textProcessor; 

И вот тот же метод внедрения, но на этот раз с использованием аннотации @Named :

 @Inject @Named("Uppercase") private TextProcessor textProcessor; 

Те же самые правила относительно синтаксиса внедрения применяются к последнему:

 // setter injection with @Default / @Alternative @Inject public void setTextProcessor(TextProcessor textProcessor) { this.textProcessor = textProcessor; } // setter injection with @Named @Inject public void setTextProcessor( @Named("Uppercase") TextProcessor textProcessor) { this.textProcessor = textProcessor; } 

На этом этапе ясно, что CDI позволит вам буквально внедрить любой тип объекта (ов)… да, в любой другой объект (ы), используя инъекцию поля, сеттера или конструктора.

Но здесь есть небольшая загвоздка: что произойдет, если зависимость требует некоторого типа начальной конфигурации или имеет свою собственную сеть зависимостей? Другими словами, как справиться с ситуацией, когда класс объявляет зависимость от целого графа объектов?

Это в точности случай класса TextApplication . Конечно, он использует объект BufferedReader , но этот объект создается в конструкторе ad-hoc. Это именно тот запах кода, который CDI пытается устранить, и причина, по которой он существует в конце концов!

Использование аннотации @Produces

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

Рассмотрим TextApplication класса TextApplication :

 @Inject public TextApplication( @UppercaseTextProcessor TextProcessor textProcessor, BufferedReader userInputReader) { this.textProcessor = textProcessor; this.userInputReader = userInputReader; } 

Определенно, это выглядит намного лучше. Злой new оператор был удален из конструктора, что делает класс намного более декларативным и более простым для тестирования. Он больше не скрывает свою прямую зависимость от объекта BufferedReader . Логично, что этот сотрудник должен быть каким-то образом создан.

Именно здесь @Produces в @Produces аннотация @Produces :

 public class BufferedReaderProducer { @Produces BufferedReader getBufferedReader() { return new BufferedReader(new InputStreamReader(System.in)); } } 

В двух словах, класс BufferedReaderProducer — это простая фабрика, которая создает объект BufferedReader . Учитывая, что метод getBufferedReader() был аннотирован аннотацией @Produces , в терминологии CDI этот метод называется производителем .

Как и в этом случае, класс BufferedReader не является реализатором какого-либо интерфейса, в частности, CDI сначала вызывает соответствующий метод производителя для получения рассматриваемого объекта, а затем внедряет его в клиентский класс.

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

Работа с пользовательскими классификаторами

Пользовательские квалификаторы необходимы, если вы хотите создать более семантические, выборочные и значимые точки внедрения в приложении. Хотя имя слегка пугает, пользовательский квалификатор — это просто простая аннотация, связанная с конкретной реализацией. На первый взгляд это похоже на аннотацию @Named которая всегда используется с одним и тем же @Named строки.

Например, для классов текстового процессора квалификаторы могут быть определены следующим образом:

 @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface LowercaseTextProcessor { } @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface UppercaseTextProcessor { } @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface ReverseTextProcessor { } 

Аннотация @Qualifier сообщает CDI, что они будут использоваться для инъекций. Но сначала их нужно применить к реализации, чтобы CDI знал, с какими типами они связаны:

 @LowercaseTextProcessor public class LowercaseTextProcessor implements TextProcessor { ... } @UppercaseTextProcessor public class UppercaseTextProcessor implements TextProcessor { ... } @ReverseTextProcessor public class ReverseTextProcessor implements TextProcessor { ... } 

Теперь, когда CDI стало известно, что, например, @UppercaseTextProcessor (аннотация) внедряет UppercaseTextProcessor (реализация), соответствующая точка внедрения может выглядеть следующим образом:

 @Inject public TextApplication( @UppercaseTextProcessor TextProcessor textProcessor, BufferedReader userInputReader) { ... } 

Довольно просто, правда? Самые большие преимущества использования пользовательских квалификаторов по сравнению с более простыми аннотациями, такими как @Named , можно свести к следующим моментам:

  1. Сделайте безопасность типов еще сильнее . Например, привязка пользовательского квалификатора к подклассу базового класса в данной иерархии гарантирует, что только подтип будет введен. @Default , @Alternative и @Named настоящее время не поддерживают эту функцию. Кроме того, ни один из них не может быть связан с политикой хранения или с юридической целью, но могут быть настроены пользовательские квалификаторы, что облегчает создание более целевых классификаторов.

  2. Разрешить инъекционные исключения неопределенности . Создать неоднозначные ситуации легко, например, если несколько реализаций помечены @Default или с @Default и тем же квалификатором для @Named . По понятным причинам в таких случаях CDI не сможет определить во время выполнения, какая реализация должна быть внедрена в целевой объект (процесс внедрения неоднозначен), и, таким образом, вызовет исключение развертывания — в терминологии CDI — внедрение зависимости исключение неоднозначности . Пользовательские квалификаторы позволяют избежать этих двусмысленностей с помощью семантических аннотаций, которые содержат метаданные, связанные с инъекциями.

  3. Соберите метаданные и свяжите их с инъекционными классами . Это включает в себя политику хранения и юридические цели, к которым могут применяться квалификаторы, такие как поля и методы.

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

Примерно то же самое можно сказать и о методах производителя: вы не хотите, чтобы несколько фабрик были разбросаны по всему вашему приложению, только потому, что вы можете, верно?

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

Резюме

Как и многие другие шаблоны проектирования, внедрение зависимостей является не зависящей от языка парадигмой, которая играет фундаментальную роль в объектно-ориентированном проектировании. Проще говоря: если вы делаете хороший объектно-ориентированный дизайн, вы просто не можете жить без DI.

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

Вполне возможно, что наиболее привлекательным аспектом CDI (и всех его реализаций) является то, что он выполняет то, что обещает, по номинальной стоимости, без каких-либо условий: простой аккуратный DI без использования внешней структуры. Более того, учитывая, что любой объект может стать инъецируемым компонентом, если он использует аннотации стандарта, довольно просто использовать CDI гораздо более продуктивным способом.

Очевидно, что использование CDI / Weld в качестве основного механизма DI в Java SE может быть освоено без удара головой о стену. Вам, конечно, будет интересно, как использовать CDI в реальном случае использования. Построение приложения для разбора строк, которое запускается в консоли, все хорошо с дидактической точки зрения и, безусловно, может быть использовано в качестве отправной точки для ознакомления с основными фактами стандарта.

Но есть еще много возможностей для покрытия. Как я уже говорил во введении, CDI позволяет вводить ресурсы Java EE с той же легкостью, что и при внедрении POJO . Эта присущая способность может быть использована для использования CDI при разработке приложений на основе базы данных.

Чтобы проиллюстрировать это, во второй части мы узнаем, как использовать стандарт для разработки приложений на основе JPA.

Будьте на связи!