Если вы следите за этим блогом, то помните, что в последнее время я пишу (и говорю) о CDI (Contexts and Dependency Injection). У CDI много аспектов, но до сих пор я акцентировал внимание на том, как начать работу с CDI в вашем окружении и как интегрировать CDI в существующее Java EE 6 приложение, а затем сфокусировался на внедрении зависимостей в CDI. Это уже третий пост про внедрение в CDI: в первом я рассказывал о внедрении по умолчанию и спецификаторах, во второй о всех возможных точках внедрения (поле, конструктор, сеттер). В этом посте я расскажу о продюсерах или "как вы можете типобезопасным способом внедрять что угодно и куда угодно".
Внедряем только бины?
До этого я показывал вам, как внедрять бины с помощью обычной аннотации @Inject
. Если вы посмотрите на пример с генерацией номера книги, который я использовал ранее, то увидите сервлет и rest сервис, в которые внедряется реализация интерфейса NumberGenerator
. Благодаря спецификаторам, сервлет может получить IsbnGenerator
, помеченный в точке внедрения спецификатором @ThirteenDigit
, а rest сервис получит IssnGenerator
, помеченный @EightDigits
(смотрите мой первый пост). Представленная диаграмма классов демонстрирует некоторую комбинацию внедрения бинов:
Но как вы видите, всё это внедрение бинов в другие бины. В CDI мы можем внедрять только бины? Нет. Вы можете внедрять что угодно и куда угодно.
Продюсеры
Да, вы можете внедрять что угодно и куда угодно. Всё, что вам нужно для этого сделать, это создать что-то, что вы хотите потом внедрить. Для этого в CDI есть продюсеры (отличная реализация паттерна Фабрика). С их помощью можно создать:
- Класс: неограниченное множество типов бина, его суперклассов и всех реализуемых им интерфейсов
- Интерфейс: неограниченное множество типов бина, рассширенные им интерфейсы, а также
java.lang.Object
- Примитивы и Java массив
Так что вы можете внедрять java.util.Date
, java.lang.String
или даже int
. Начнем с создания и внедрения некоторых типов данных и примитивов.
Создание типов данных и примитивных типов
Один из примеров того, как внедрить что угодно и куда угодно, это внедрение типов данных и примитивов. Ну что, давайте попробуем внедрить String
и int
. Для этого мне нужно добавить дополнительные классы в нашу модель. До этого IsbnGenerator
создавал случайное число в формате 13-124-454644-4. Этот номер состоит из строки, выступающей в роли префикса (13-124), и целого значения — постфикса (4). Следующая диаграмма классов показывает два новых класса PrefixGenerator
и PostfixGenerator
, которые будут использоваться для генерации номера:
Если мы посмотрим, например на код PrefixGenerator
, то увидим класс, который сам не помечен никаким спецификатором, в отличие от его методов. Метод getIsbnPrefix
возвращает строку, которая специфицирована аннотацией @ThirteenDigits
. Эта строка создается с помощью CDI (благодаря @Produces
), а это значит, что вы можете внедрять её с помощью @Inject
и её спецификатора (@ThirteenDigits
).
public class PrefixGenerator {
@Produces @ThirteenDigits
public String getIsbnPrefix() {
return "13-84356";
}
@Produces @EightDigits
public String getIssnPrefix() {
return "8";
}
}
А теперь внимательно посмотрите на класс PostfixGenerator
. Этот код аналогичен предыдущему, за исключением того, что он создает int
, который потом может быть внедрен.
public class PostfixGenerator {
@Produces @ThirteenDigits
public int getIsbnPostfix() {
return 13;
}
@Produces @EightDigits
public int getIssnPostfix() {
return 8;
}
}
А теперь поменяем логику генерации ISBN номера. В бин IsbnGenerator
внедряются String
и int
c помощью @Inject
и @ThirteenDigits
. Теперь вы понимаете, что значит строгая типизация в CDI. Используя этот синтаксис (@Inject @ThirteenDigits
), CDI знает, что нужно внедрить на место String
, а что на место int и какую использовать реализацию NumberGenerator
.
@ThirteenDigits
public class IsbnGenerator implements NumberGenerator {
@Inject @ThirteenDigits
private String prefix;
@Inject @ThirteenDigits
private int postfix;
public String generateNumber() {
return prefix + "-" + Math.abs(new Random().nextInt()) + "-" + postfix;
}
}
Внедрение ресурсов Java EE через поля продюсеров
Итак, если CDI может внедрять что угодно и куда угодно, если он даже умеет внедрять строки и целые числа, то как насчет ресурсов Java EE? Это другая история, и продюсеры нам помогут в этом. Как я уже говорил ранее, в CDI всё типобезопасно, так что в CDI нет никакого способа внедрить ресурс по его JNDI-имени, например, так: @Inject(name="jms/OrderQueue")
. Стандартный пример — менеджер сущностей. Если вы не используете CDI, то в Java EE 6 вы можете внедрить его следующим образом:
@Stateless
public class ItemEJB {
@PersistenceContext(unitName = "cdiPU")
private EntityManager em;
...
}
Так почему же невозможно сделать обычный @Inject
для менеджера сущностей? Если вы помните мой первый пост о неоднозначности внедрения, то тут та же проблема. У вас может быть несколько единиц персистентности (именованных обычной строкой), и если вы просто напишите @Inject
, то CDI не сможет понять, какой именно из них нужно внедрить. Вместо этого вы должны создавать менеджер сущностей первым и как-то его именовать (если не хотите использовать @Default
):
public class DatabaseProducer {
@Produces
@PersistenceContext(unitName = "cdiPU")
@BookStoreDatabase
private EntityManager em;
}
Класс DatabaseProducer
использует @PersistenceContext
для внедрения менеджера сущностей с единицей персистентности cdiPU. Он будет помечен спецификатором (@BookStoreDatabase
) и инициализирован, после чего вы можете внедрять его в EJB следующим образом:
@Stateless
public class ItemEJB {
@Inject
@BookStoreDatabase
private EntityManager em;
...
}
Другой хороший пример — это создание фабрик JMS и адресатов. Аннотация @Resource
позволяет вам получить JNDI-ссылку на JMS-фабрику или адресата, спецификатор @Order
именует её, а @Produces
позволяет в дальнейшем её внедрить:
public class JMSResourceProducer {
@Produces @Order @Resource(name = "jms/OrderConnectionFactory")
private QueueConnectionFactory orderConnectionFactory;
@Produces @Order @Resource(name = "jms/OrderQueue")
private Queue orderQueue;
}
Теперь ваш EJB может использовать типобезопасный @Inject
:
@Stateless
public class ItemEJB {
@Inject @Order
private QueueConnectionFactory orderConnectionFactory;
@Inject @Order
private Queue orderQueue;
...
}
Создание Java EE ресурсов с помощью методов продюсеров
Рассмотренные примеры достаточно простые: создаем поле и затем можем его внедрять. Это называется продюсирующим полем. Но иногда вам необходима более сложная бизнес-логика создания бина, и это мы будем называть методом-продюсером. Давайте посмотрим на другой пример. Когда вам нужно отправить JMS-сообщение, вы всегда внедряете JMS-фабрику и адресата (очередь или топик), создаете соединение, потом сессиию и так далее, пока не отправите сообщение. Поскольку этот код часто повторяется и может содержать ошибки, то почему бы его не вынести куда-нибудь в отдельный класс и не создавать с его помощью сессию и другие компоненты, необходимые для отправки сообщения? Этот класс мог бы выглядеть, например, так:
public class JMSResourceProducer {
@Resource(name = "jms/OrderConnectionFactory")
private QueueConnectionFactory orderConnectionFactory;
@Produces @Order @Resource(name = "jms/OrderQueue")
private Queue orderQueue;
@Produces @Order
public QueueConnection createOrderConnection() throws JMSException {
return orderConnectionFactory.createQueueConnection();
}
@Produces @Order
public QueueSession createOrderSession(@Order QueueConnection conn) throws JMSException {
return conn.createQueueSession(true, Session.AUTO_ACKNOWLEDGE);
}
}
Во-первых, класс использует @Resource
для получения ссылок на QueueConnectionFactory
и Queue
. По тем же причинам, которые я описывал выше, у вас может быть несколько JMS-фабрик и адресатов, и вы должны различать их по JNDI-имени. И так как CDI не позволяет указывать имя в точке внедрения, вы должны использовать @Resource
вместо @Inject
. Метод createOrderConnection
использует QueueConnectionFactory
для создания QueueConnection
и дальнейшего его внедрения (попутно именуя его @Order
). Если вы внимательно посмотрите на метод createOrderSession, то увидите, что его параметр — это созданный раннее для внедрения QueueConnection
и с его помощью создается QueueSession
. В итоге достаточно просто внедрить JMS-сессию во внешний компонент без её создания:
@Stateless
public class ItemEJB {
@Inject @Order
private QueueSession session;
@Inject @Order
private Queue orderQueue;
private void sendOrder(Book book) throws Exception {
QueueSender sender = session.createSender(orderQueue);
TextMessage message = session.createTextMessage();
message.setText(marshall(book));
sender.send(message);
}
...
}
Создание и… утилизация
Вы можете сказать: "Ок, у меня есть внешний класс для черновой работы и создания соединения и сессии… но кто их будет закрывать?" Действительно, кто-то должен освободить эти ресурсы, закрыв их. И в этот момент CDI демонстрирует ещё немного магии: @Disposes
.
public class JMSResourceProducer {
@Resource(name = "jms/OrderConnectionFactory")
private QueueConnectionFactory orderConnectionFactory;
@Produces @Order @Resource(name = "jms/OrderQueue")
private Queue orderQueue;
@Produces @Order
public QueueConnection createOrderConnection() throws JMSException {
return orderConnectionFactory.createQueueConnection();
}
@Produces @Order
public QueueSession createOrderSession(@Order QueueConnection conn) throws JMSException {
return conn.createQueueSession(true, Session.AUTO_ACKNOWLEDGE);
}
public void closeOrderSession(@Disposes @Order QueueConnection conn) throws JMSException {
conn.close();
}
public void closeOrderSession(@Disposes @Order QueueSession session) throws JMSException {
session.close();
}
}
Для того чтобы попросить CDI закрыть ресурс, вам нужно просто определить метод, аналогичный тому, который создавал этот ресурс (createOrderSession(@Order QueueConnection conn)
создаёт сессию, а closeOrderSession(@Order QueueConnection conn)
для её закрытия) и добавить аннотацию @Disposes
. CDI будет утилизировать ресурсы в правильном для вас порядке (сначала сессия, потом соединение). Я об этом не упоминал, но CDI создает и утилизирует ресурсы в зависимости от их области действия (resquest, session, application, conversation...). Но об этом уже в другой раз. (информация есть в Beginning Java EE 7 [перевод] — прим. пер.)
Заключение
Как вы поняли из моих предыдущих постов (часть 1, часть 2), CDI — это о внедрении зависимостей. До сих пор я показывал, как внедрять бины, но теперь вы знаете как можно внедрить что угодно (строки, примитивы, менеджеры сущностей, JMS-фабрики...) куда угодно (POJO, сервлеты, EJB...). Вам просто нужно создавать то, что вы хотите внедрять (используя либо продюсирующие поля, либо методы-продюсеры).
Исходный код
Скачайте код и расскажите, что вы о нем думаете.