Я лично сомневаюсь в преимуществах внедрения зависимости. С одной стороны, я признаю его полезность в определенных контейнерных средах, таких как 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.Class
Ceylon 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. Есть три основных стратегии для решения этой проблемы:
- Скомпилируйте и запустите с плоским classpath с помощью
--flat-classpath
. Это заставляет Цейлон работать как Java и лишает нас изоляции модулей. - Используйте
--export-maven-dependencies
для реэкспорта всех зависимостей каждого модуля Maven. - Используйте
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 не использует прокси, поэтому нет необходимости использовать интерфейс вместо конкретного класса.
Вывод
Если вы хотите внедрить зависимости в Цейлон, ясно, что у вас есть как минимум два превосходных варианта.