После статьи о том, как начать работу с CDI в вашем окружении и нескольких советов о том, как интегрировать CDI в существующее Java EE 6 приложение, я хочу поговорить о внедрении зависимостей. Да, о простом внедрении или о том, как провести внедрение одного бина в другой. Из этой серии трех статей (Часть 2, Часть 3) вы увидите, что есть множество различных способов: давайте начнем с самого простого — обыкновенного внедрения.
Внедрение по умолчанию
Самый простой способ внедрения… простой. У вас есть что-то, и вы что-то внедряете в это. Почему я использую слово что-то? Потому что до Java EE 5 вы могли внедрять только ресурсы (EntityManager, Datasource, JMS фабрики...) в определенные компоненты (EJB и сервлеты). С CDI, вы можете внедрять практически что угодно во что угодно.
Для этой статьи я использовал следующее программное обеспечение:
- Java SE 1.6.0_23
- GlassFish 3.1
- Maven 3.0.2
Для иллюстрации внедрения я буду использовать тот же пример, что и в предыдущих статьях и в своей книге Java EE 6 (этот же пример Antonio использует и в книге Beginning Java EE 7 [перевод] — прим. пер.).
На диаграмме классов показаны следующие компоненты:
Book
— это простая JPA сущность с некоторыми атрибутами и именованными запросамиItemEJB
— это EJB (без интерфейса), выполняющий CRUD операции с Book при помощи EntityManager'аIsbnGenerator
— простой POJO класс, который генерирует случайное число ISBN (используется для Book)ItemRestService
с аннотацией@Path
(обозначающей REST сервис в JAX-RS), делегирующий CRUD операцииItemEJB
ItemServlet
— это сервлет, который используетItemEJB
для отображения всех книг из базы данных
Как вы видите, за исключением EntityManager'а, который внедряется с помощью @PersistenceContext
, все остальные компоненты внедряются аннотацией @Inject
. Вот несколько строк кода ItemEJB
, получающих ссылку на EntityManager:
@Stateless
public class ItemEJB {
@PersistenceContext(unitName = "cdiPU")
private EntityManager em;
...
}
ItemServlet
и ItemRestService
очень похожи, так как в оба внедряются ссылки на ItemEJB
и IsbnGenerator
:
@WebServlet(urlPatterns = "/itemServlet")
public class ItemServlet extends HttpServlet {
@Inject
private IsbnGenerator numberGenerator;
@Inject
private ItemEJB itemEJB;
...
}
IsbnGenerator
— это самый обычный POJO. Он ни от кого не наследуется и никак не проаннотирован:
public class IsbnGenerator {
public String generateNumber () {
return "13-84356-" + Math.abs(new Random().nextInt());
}
}
Во всех этих случаях есть только одна реализация на выбор (есть только один ItemEJB
и один IsbnGenerator
). Если у вас только одна реализацию, CDI сможет её внедрить. Далее мы поговорим о внедрении по умолчанию.
На самом деле код:
@Inject IsbnGenerator numberGenerator
мог бы быть написан вот так:
@Inject @Default IsbnGenerator numberGenerator
@Default
— это встроенный спецификатор, который информирует CDI о том, что внедрять бин нужно реализацией по умолчанию. Если вы определяете бин без спецификатора, он будет автоматически специфицирован @Default
. Следующий код идентичен предыдущему:
@WebServlet(urlPatterns = "/itemServlet")
public class ItemServlet extends HttpServlet {
@Inject @Default
private IsbnGenerator numberGenerator;
...
}
@Default
public class IsbnGenerator {
public String generateNumber () {
return "13-84356-" + Math.abs(new Random().nextInt());
}
}
Если для внедрения у вас есть только одна реализация IsbnGenerator
, то сработает обработчик по умолчанию и обыкновенный @Inject
сделает свою работу. Но иногда необходимо выбирать между несколькими реализациями, вот тогда вступает в игру спецификатор.
В этой статье я использую термин бин, но если быть более точным, я должен говорить управляемый бин (например бин, управляемый CDI). ManagedBeans были введены в Java EE 6.
Неоднозначные внедрения и спецификаторы
Для какого-то типа бинов может быть несколько бинов, реализующих этот тип. Например, наше приложение может иметь две реализации интерфейса NumberGenerator
: IsbnGenerator
, генерирующий 13-значный номер и IssnGenerator
, генерирующий 8-значный. Компонент, которому необходимо генерировать 13-значный номер должен каким-то образом различать две реализации. Один из вариантов — явно указывать класс (IsbnGenerator
), но тогда создается жесткая зависимость между компонентой и реализацией. Другой вариант — это описание внедрения подходящего бина во внешней XML-конфигурации. CDI же использует аннотации спецификаторы для получения строгой типизации и слабой связности.
В данной статье я использую внедрение через поле (атрибут) класса, но с CDI вы можете также использовать внедрение через сеттеры или конструкторы.
Предположим, для каких-то целей ItemServlet
создает книги с 13-значным ISBN номером, а ItemRestService
— с 8-значным ISSN номером. В оба класса (ItemServlet
и ItemRestService
) внедрены ссылки на один и тот же интерфейс NumberGenerator
, но какая в итоге реализация будет использоваться? Вы не знаете? CDI тоже не знает, и вы получите сообщение об ошибке:
Ambiguous dependencies for type [NumberGenerator] with qualifiers [@Default] at injection point [[field] @Inject private ItemRestService.numberGenerator]. Possible dependencies [[Managed Bean [class IsbnGenerator] with qualifiers [@Any @Default], Managed Bean [class IssnGenerator] with qualifiers [@Any @Default]]].
Это означает, что нужно убрать неоднозначность и указать CDI, какой бин и где должен внедряться. Если вы раньше использовали Spring, то первое, что придёт вам на ум, это "давайте использовать beans.xml". Но, как рассказывается в этом посте, "beans.xml не тот XML, где определяются бины". С CDI вы должны использовать спецификаторы (аннотации).
Есть три встроенных в CDI спецификатора:
@Default
: если бин не содержит в определении спецификатор, то он имеет@Default
спецификатор@Any
: позволяет приложению определять спецификаторы динамически@New
: позволяет приложению получить новый специфицированный бин (с CDI 1.1 спецификатор@New
считается устаревшим — прим. пер.)
Спецификатор — это семантическая конструкция, связывающая тип с некоторой его реализацией. Например, вы могли бы ввести спецификатор для представления генератора 13-значных номеров или генератора 8-значных номеров. В Java спецификаторы представляются аннотациями, определенными как @Target({FIELD, TYPE, METHOD})
и @Retention(RUNTIME)
. Они объявляются с указанием мета-аннотации @javax.inject.Qualifier
следующим образом:
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface EightDigits {
}
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface ThirteenDigits {
}
Как вы видите, я только что очень просто определил два спецификатора. Как я теперь могу их использовать? Давайте посмотрим на диаграмму:
Прежде всего, спецификатором необходимо проаннотировать соответствующую реализацию. Как вы видите, @ThirteenDigit
аннотирует IsbnGenerator
, а @EightDigit
— IssnGenerator
:
@EightDigits
public class IssnGenerator implements NumberGenerator {
public String generateNumber() {
return "8-" + Math.abs(new Random().nextInt());
}
}
@ThirteenDigits
public class IsbnGenerator implements NumberGenerator {
public String generateNumber() {
return "13-84356-" + Math.abs(new Random().nextInt());
}
}
Затем компоненты, в которые внедряются ссылки на интерфейс NumberGenerator
, необходимо реализовывать следующим образом:
@WebServlet(urlPatterns = "/itemServlet")
public class ItemServlet extends HttpServlet {
@Inject @ThirteenDigits
private NumberGenerator numberGenerator;
...
}
@Path("/items")
@ManagedBean
public class ItemRestService {
@Inject @EightDigits
private NumberGenerator numberGenerator;
...
}
Вам не нужна внешняя конфигурация. Вот почему CDI предписывает использовать строгую типизацию. Вы можете переименовывать ваши реализации как захотите, точки внедрения при этом не будут меняться (это слабая связность). Обратите также внимание на то, что бин может быть объявлен несколькими спецификаторами. Как вы можете видеть, CDI — это элегантный способ иметь типобезопасное внедрение зависимостей. Но если вы начинаете создавать аннотации каждый раз, когда вам нужно что-то внедрить, в конечном итоге ваш код будет трудночитаемым. В этом случае вам помогут перечисления.
Спецификаторы с перечислениями
Каждый раз, когда вам необходимо выбирать между реализациями, вы создаете аннотацию. Тогда, если вам нужен дополнительный генератор 2-значных номеров или генератор 10-значных номеров, вы создаете всё больше и больше аннотаций. Похоже, что мы переходим от XML ада к аду аннотаций! Один из способов избежать их размножения — это использовать перечисления следующим образом:
@WebServlet(urlPatterns = "/itemServlet")
public class ItemServlet extends HttpServlet {
@Inject @NumberOfDigits(Digits.THIRTEEN)
private NumberGenerator numberGenerator;
...
}
@Path("/items")
@ManagedBean
public class ItemRestService {
@Inject @NumberOfDigits(Digits.EIGHT)
private NumberGenerator numberGenerator;
...
}
@NumberOfDigits(Digits.THIRTEEN)
public class IsbnGenerator implements NumberGenerator {
public String generateNumber() {
return "13-84356-" + Math.abs(new Random().nextInt());
}
}
@NumberOfDigits(Digits.EIGHT)
public class IssnGenerator implements NumberGenerator {
public String generateNumber() {
return "8-" + Math.abs(new Random().nextInt());
}
}
Как вы видите, я избавился от спецификаторов @ThirteenDigits
и @EightDigits
и использую единственный спецификатор @NumberOfDigit
, в котором перечисление используется как значение аннотации. Вот код, который вы должны написать:
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface NumberOfDigits {
Digits value();
}
public enum Digits {
TWO,
EIGHT,
TEN,
THIRTEEN
}
Заключение
Я не знаю как вы, но я люблю CDI. Мне очень нравится, как CDI типобезопасно связывает бины обычной Java и без XML. Да, я признаю, что не всё так красиво. Первое, что я вижу, — это разрастание числа аннотаций в вашем коде. Благодаря перечислениям это можно ограничинить. Второе — это поддержка в IDE. Ваша IDE должна знать, что:
@Inject @NumberOfDigits(Digits.EIGHT) NumberGenerator numberGenerator
ссылается на IssnGenerator
. Что касается поддержки в IDE, то я не могу сказать, что глубоко разбирался в этой теме. Я использую Intellij IDEA, и в нём поддержка CDI просто удивительна. Я могу перемещаться между бинами, не думая об их реализации. Я полагаю, в NetBeans какая-то поддержка тоже реализована… но мне интересно, есть ли она в Eclipse ;o) (статья опубликована в 2011 году. — прим. пер.)
В следующей статье я рассмотрю различные точки внедрения.
Исходный код
Скачайте код и расскажите, что вы о нем думаете.