Внедрение зависимостей в CDI. Часть 3

в 16:39, , рубрики: cdi, dependency injection, java, javaee, Блог компании AT Consulting, Программирование

Если вы следите за этим блогом, то помните, что в последнее время я пишу (и говорю) о CDI (Contexts and Dependency Injection). У CDI много аспектов, но до сих пор я акцентировал внимание на том, как начать работу с CDI в вашем окружении и как интегрировать CDI в существующее Java EE 6 приложение, а затем сфокусировался на внедрении зависимостей в CDI. Это уже третий пост про внедрение в CDI: в первом я рассказывал о внедрении по умолчанию и спецификаторах, во второй о всех возможных точках внедрения (поле, конструктор, сеттер). В этом посте я расскажу о продюсерах или "как вы можете типобезопасным способом внедрять что угодно и куда угодно".

COFFEE_BEANS

Внедряем только бины?

До этого я показывал вам, как внедрять бины с помощью обычной аннотации @Inject. Если вы посмотрите на пример с генерацией номера книги, который я использовал ранее, то увидите сервлет и rest сервис, в которые внедряется реализация интерфейса NumberGenerator. Благодаря спецификаторам, сервлет может получить IsbnGenerator, помеченный в точке внедрения спецификатором @ThirteenDigit, а rest сервис получит IssnGenerator, помеченный @EightDigits (смотрите мой первый пост). Представленная диаграмма классов демонстрирует некоторую комбинацию внедрения бинов:

CDI_3_1

Но как вы видите, всё это внедрение бинов в другие бины. В 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, которые будут использоваться для генерации номера:

CDI_3_2

Если мы посмотрим, например на код 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...). Вам просто нужно создавать то, что вы хотите внедрять (используя либо продюсирующие поля, либо методы-продюсеры).

Исходный код

Скачайте код и расскажите, что вы о нем думаете.

Автор: AT Consulting

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js