Статьи

Инъекция зависимостей на Цейлоне со сваркой

Я лично сомневаюсь в преимуществах внедрения зависимости. С одной стороны, я признаю его полезность в определенных контейнерных средах, таких как Java EE. (Кстати, я был автором спецификации CDI 1.0 с моей группой экспертов JCP.) С другой стороны, учитывая характер того, над чем я работал последние несколько лет, у меня нет использование для этого в моих собственных программах.

Но есть много людей, которые клянутся через инъекцию зависимости и спрашивают меня, что Цейлон предлагает в этой области. Краткий ответ: ничего особенного; Цейлонский SDK построен вокруг понятия модульных библиотек. Он не предлагает ни рамки, ни контейнера. Это делает SDK настолько универсальным, насколько это возможно, то есть его можно повторно использовать из любой другой контейнерной среды (скажем, Java EE, vert.x, OSGi или любой другой).

Так что если вы хотите внедрить зависимости в Ceylon сегодня, вам придется использовать контейнер, написанный на Java. К счастью, Ceylon 1.2 обладает таким превосходным взаимодействием с Java, что это практически не приводит к трениям. Конечно , кто — то будет писать зависимость инъекционного контейнера на Цейлоне нибудь , но, как мы о том, чтобы увидеть, нет никакой срочности вообще.

Я собираюсь исследовать:

  • Weld , эталонная реализация CDI, разработанная моими коллегами из Red Hat, и,
  • в интересах предоставления равного времени «конкуренту», Google Guice , первоначально написанный моим другом Бобом Ли, который оказал одно из основных влияний на спецификацию CDI.

Это мои любимые контейнеры для Java, хотя, конечно, у Spring есть легионы фанатов. Возможно, я найду время поиграть с ним в другой день.

Вы можете найти пример кода в следующем Git-репозитории:

https://github.com/ceylon/ceylon-examples-di

сваривать

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

Модуль дескриптора для сварки

Weld предоставляет толстую банку в Maven Central, что делает его особенно простым в использовании на Цейлоне. Я использовал следующий дескриптор модуля, чтобы загрузить Weld из Maven Central и импортировать его в свой проект:

native("jvm")
module weldelicious "1.0.0" {
    import "org.jboss.weld.se:weld-se" "2.3.1.Final";
    import ceylon.interop.java "1.2.0";
}

Где org.jboss.weld.seэто Maven идентификатор группы , и weld-seявляется Maven артефакта идентификатор . (Я понятия не имею, что на самом деле означают эти вещи, я просто знаю, что их два).

Я также импортировал модуль Ceylon SDK, ceylon.interop.javaпотому что собираюсь использовать его javaClass()функцию.

Bootstrapping Weld

Хотя это не является частью спецификации CDI, Weld предлагает очень простой API для создания контейнера. Я скопировал / вставил следующий код из StackOverflow:

import org.jboss.weld.environment.se { Weld }

shared void run() {

    value container = Weld().initialize();

    //do stuff with beans
    ...

    container.shutdown();

}

Я пытался запустить эту функцию.

Попался!

Так же , как и любой другой разработчик CDI когда — либо , я забыл beans.xmlфайл. К счастью, Weld дал мне довольно четкое сообщение об ошибке. Возможно, не так поэтично, как «se te escapó la tortuga» , но достаточно хорошо, чтобы напомнить мне об этом требовании спецификации. (Да, спецификацию я написал.)

Чтобы решить эту проблему, я добавил пустой файл с именем beans.xmlв каталог resource/weldelicious/ROOT/META-INF, который является волшебным местом для использования, если вы хотите, чтобы Цейлон поместил файл в META-INFкаталог архива модуля.

Определение сварных бобов

Я определил следующий интерфейс для компонента, который я надеялся внедрить:

interface Receiver {
    shared formal void accept(String message);
}

Далее я определил бин, который зависит от экземпляра этого интерфейса:

import javax.inject { inject }

inject class Sender(Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

( injectАннотация — это то, что вы пишете @Injectна Java.)

Наконец, нам нужен компонент, который реализует Receiver:

class PrintingReceiver() satisfies Receiver {
    accept = print;
}

Получение и вызов боба

Возвращаясь к run()функции, я добавил некоторый код для получения a Senderиз контейнера и вызвал send():

import org.jboss.weld.environment.se { Weld }
import ceylon.interop.java { type = javaClass }

shared void run() {

    value container = Weld().initialize();

    value sender 
            = container
                .select(type<Sender>())
                .get();

    sender.send();

    weld.shutdown();

}

Обратите внимание, что я использую javaClass()функцию, чтобы получить экземпляр типа java.lang.ClassCeylon Sender. Альтернативный подход, который использует только API-интерфейс CDI и который также работает для универсальных типов, заключается в использовании javax.enterprise.inject.TypeLiteral:

value sender 
        = container
            .select(object extends TypeLiteral<Sender>(){})
            .get();

К сожалению, это немного более многословно.

Инъекция именованного конструктора

Используя небольшое быстрое исправление в IDE, мы можем преобразовать Senderкласс в класс с конструктором по умолчанию:

class Sender {
    Receiver receiver;
    inject shared new (Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

Что касается Weld, это то же самое, что у нас было раньше.

Но мы даже можем дать нашему конструктору имя:

class Sender {
    Receiver receiver;
    inject shared new inject(Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

Из-за непредвиденной случайности, это на самом деле просто работает.

Способ и полевая инъекция

Я не думаю, что метод или полевая инъекция — это очень естественная вещь на Цейлоне, и поэтому я не рекомендую это делать. Тем не менее, это работает, только если вы пометите любые поля, инициализированные с помощью инъекции, lateаннотацией:

Это работает, но не очень цейлонски:

class Sender() {
    inject late Receiver receiver;
    shared void send() => receiver.accept("Hello!");
}

Это тоже работает:

class Sender() {
    late Receiver receiver;
    inject void init(Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

Использование производителя CDI

Хорошая вещь об использовании Цейлона с Weld заключается в том, что вы можете использовать producesаннотацию для функции верхнего уровня.

import javax.enterprise.inject { produces }

produces Receiver createReceiver() 
        => object satisfies Receiver {
            accept = print;
        };

CDI квалификаторы

Мы можем определить аннотации квалификатора CDI на Цейлоне:

import javax.inject { qualifier }

annotation Fancy fancy() => Fancy();
final qualifier annotation class Fancy() 
        satisfies OptionalAnnotation<Fancy> {}

Квалификационная аннотация должна применяться как в точке внедрения, так и к функции компонента или производителя. Сначала я прокомментировал класс бобов:

fancy class FancyReceiver() satisfies Receiver {
    accept(String message) 
            => print(message + " \{BALLOON}\{PARTY POPPER}");
}

Затем я попытался аннотировать введенный параметр инициализатора:

//this doesn't work!
inject class Sender(fancy Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

К сожалению, это не сработало. При компиляции в Java-байт-код Ceylon фактически помещает эту fancyаннотацию в сгенерированный метод получения Sender, а не в параметр, а Weld ищет только аннотации квалификатора для введенных параметров. Мне пришлось использовать конструктор для правильной работы классификатора:

//this does work
class Sender {
    Receiver receiver;
    inject shared new (fancy Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

К сведению, аннотации квалификаторов также работают с внедрением метода. Они не работают с полевой инъекцией.

Это было единственное разочарование, которое я испытал при использовании Weld с Цейлоном, и я думаю, что уже знаю, как решить эту проблему в Цейлоне 1.2.1.

Scoped бобы

Вы можете определить bean-объекты области видимости (bean-компоненты с тем, что спецификация CDI называет нормальной областью действия ) в Ceylon, просто применив аннотацию области действия к компоненту:

import javax.enterprise.context { applicationScoped }

applicationScoped
class PrintingReceiver() satisfies Receiver {
    accept = print;
}

Однако здесь есть кое-что, что следует соблюдать осторожно: CDI создает прокси для bean-объектов с областями видимости, и, поскольку операции класса Ceylon по умолчанию являются «конечными», у вас есть выбор между:

  • аннотировать все операции компонента defaultили
  • внедрение интерфейса вместо конкретного класса бинов.

Я думаю, что второй вариант — гораздо лучший путь, и, возможно, даже лучший подход в Java.

Конечно, то же самое относится и к bean-компонентам с перехватчиками CDI или декораторами, хотя я этого не проверял.

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

Guice

Guice тоже было довольно легко настроить, хотя я потратил немного времени на Maven.

Модуль переопределений для Guice

Guice не входит в толстую банку, поэтому нам придется столкнуться с общей проблемой при использовании модулей Maven из Цейлона. Maven разработан для плоского Java-пути к классам, поэтому модуль Maven не содержит метаданных о том, какие из его зависимостей реэкспортируются через его общедоступный API. Есть три основных стратегии для решения этой проблемы:

  1. Скомпилируйте и запустите с плоским classpath с помощью --flat-classpath. Это заставляет Цейлон работать как Java и лишает нас изоляции модулей.
  2. Используйте --export-maven-dependenciesдля реэкспорта всех зависимостей каждого модуля Maven.
  3. Используйте overrides.xmlфайл, чтобы явно указать, какие зависимости реэкспортируются.

Мы собираемся пойти с вариантом 3, так как это самый сложный.

Но подождите — вы, должно быть, думаете — XML ?! И да, не волнуйтесь, мы ненавидим XML так же сильно, как и вы. Это временная мера, пока на Цейлоне не появятся реальные сборки . Как только у нас будут сборки, вы сможете переопределить зависимости модулей в дескрипторе сборки Ceylon.

В любом случае, после этой многословной преамбулы все, что мне нужно было сделать, это отметить javax.injectкак общую зависимость:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
    <module groupId="com.google.inject" 
         artifactId="guice" 
            version="4.0">
        <share groupId="javax.inject" 
            artifactId="javax.inject"/>
    </module>
</overrides>

Вы можете скопировать и вставить вышеупомянутый шаблон в свои собственные проекты Цейлона и Guice.

Дескриптор модуля для Guice

Следующий дескриптор модуля извлекает Guice и его зависимости из Maven Central и импортирует Guice в проект:

native("jvm")
module guicy "1.0.0" {
    import "com.google.inject:guice" "4.0";
    import ceylon.interop.java "1.2.0";
}

Код, который мы можем использовать из примера Weld

Поскольку Guice признает injectаннотацию , определенную в javax.inject, мы можем использовать определение Sender, Receiverи PrintingReceiverмы начали с выше.

import javax.inject { inject }

inject class Sender(Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

interface Receiver {
    shared formal void accept(String message);
}

class PrintingReceiver() satisfies Receiver {
    accept = print;
}

Bootstrapping Guice

У Guice есть понятие объекта модуля , который имеет набор привязок типов к объектам. В отличие от Weld, который автоматически сканирует наш архив модулей в поисках bean-компонентов, привязки должны быть явно зарегистрированы в Guice.

import ceylon.interop.java {
    type = javaClass
}
import com.google.inject {
    AbstractModule,
    Guice {
        createInjector
    },
    Injector
}

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {
            bind(type<Receiver>()).to(type<PrintingReceiver>());
        }
    });

Этот код связывает реализацию PrintingReceiverс интерфейсом Receiver.

Получение и вызов объекта

Теперь легко получить и вызвать связанный с контейнером экземпляр Sender:

import ceylon.interop.java {
    type = javaClass
}

shared void run() {
    value sender = injector.getInstance(type<Sender>());
    sender.send();
}

Мы снова используем javaClass(), но у Guice есть свои TypeLiteral. (Для записи CDI украли TypeLiteralу Guice.)

import com.google.inject {
    Key,
    TypeLiteral
}

shared void run() {
    value key = Key.get(object extends TypeLiteral<Sender>(){});
    value sender = injector.getInstance(key);
    sender.send();
} 

Конструктор инъекций

Инъекция в конструкторы по умолчанию работает и выглядит точно так же, как и для Weld. Однако внедрение в именованные конструкторы не работает с Ceylon 1.2.0 и Guice 4.0. Это довольно легко исправить с нашей стороны, и поэтому оно должно работать в Цейлоне 1.2.1.

Способ и полевая инъекция

Создатели Guice сильно предпочитают инъекцию конструктора, которая, как мы наблюдали, также более естественна для Цейлона. Но метод и метод ввода поля работают хорошо, как и в случае со сваркой, если вы пометите введенное поле late.

Методы провайдера

Guice сканирует объект модуля на наличие аннотированных методов provides.

import com.google.inject {
    AbstractModule,
    Guice {
        createInjector
    },
    Injector,
    provides
}

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {}
        provides Receiver createReceiver()
                => object satisfies Receiver {
                    accept = print;
                };
    });

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

Обязательные аннотации

Связывающие аннотации Guice работают почти так же, как аннотации спецификаторов CDI (поскольку именно там CDI скопировал их). Код для определения привязки аннотации точно такой же, как для Weld.

import javax.inject { qualifier }

annotation Fancy fancy() => Fancy();
final binding annotation class Fancy() 
        satisfies OptionalAnnotation<Fancy> {}

Ключевая аннотация должна быть указана при определении привязки:

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {
            bind(type<Receiver>())
                .to(type<PrintingReceiver>());
            bind(type<Receiver>())
                .annotatedWith(Fancy()) //binding annotation
                .to(type<FancyReceiver>());
        }
    });

Как и в Weld, аннотации квалификаторов работают с внедрением конструктора или метода, но в настоящее время не работают с параметром инициализатора или внедрением поля.

Scoped бобы

Как и CDI, у Guice есть области видимости.

import com.google.inject { singleton }

singleton
class PrintingReceiver() satisfies Receiver {
    accept = print;
}

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

Вывод

Если вы хотите внедрить зависимости в Цейлон, ясно, что у вас есть как минимум два превосходных варианта.