В рамках JSR-299 “Contexts and Dependency Injection for the Java EE platform” (ранее WebBeans) была разработана спецификация описывающая реализацию паттерна внедрения зависимости, включенная в состав Java EE 6. Эталонной реализацией является фреймворк Weld, о котором и пойдет речь в данной статье.
К сожалению в сети не так много русскоязычной информации о нем. Скорее всего это связано с тем, что Spring IOC является синонимом dependency injection в Java Enterprise приложениях. Есть конечно еще Google Guice, но он тоже не так популярен.
В статье хотелось бы рассказать об основных преимуществах и недостатках Weld.
В начале стоит упомянуть про JSR-330 “Dependency Injection for Java” спецификацию, разработанную инженерами из SpringSource и Google, определяющую базовые механизмы для реализации DI в Java приложениях. Как и Spring и Guice, Weld использует аннотации предусмотренные данной спецификацией.
Weld может работать не только с Java EE приложениями, но и с обычным окружением Java SE. Естественно есть поддержка Tomcat, Jetty; в официальной документации описаны подробные инструкции по настройке.
В Contexts and Dependency Injection (CDI) невозможно инъектировать бин через его имя в виде строки, как это например делается в Spring через Qualifier. Вместо этого используется шаблон qualifier annotations (о котором ниже). С точки зрения создателей CDI, это более typesafe подход, позволяющий избежать некоторых ошибок и обеспечивающий гибкость DI.
Для того, чтобы задействовать CDI нужно создать файл beans.xml в директории WEB-INF для веб-приложения (или в META-INF для Java SE):
<beans xmlns=«java.sun.com/xml/ns/javaee»
xmlns:xsi=«www.w3.org/2001/XMLSchema-instance»
xsi:schemaLocation=«java.sun.com/xml/ns/javaee java.sun.com/xml/ns/javaee/beans_1_0.xsd»>
</beans>
Как и в Spring, у бинов в контексте CDI есть свой скоуп в течении которого они существуют. Скоуп задается с помощью аннотаций из пакета javax.enterprise.context:
@ConversationScoped представляет собой определенный промежуток времени взаимодействия пользователя с конкретной вкладкой в браузере. Поэтому он чем-то похож на жизненный цикл сессии, но важное отличие в том, что старт “сессии” задается вручную. Для этого объявлен интерфейс javax.enterprise.context.Conversation, который определяет методы start(), end(), а также setTimeout(long), для закрытия сессии по истечении времени.
Конечно же, есть и Singleton, который находится в пакете javax.inject, т.к. является частью спецификации JSR-330. В реализации CDI данного скоупа есть одна особенность: в процессе инъекции клиент получает ссылку на реальный объект созданный контейнером, а не proxy. В результате чего могут быть проблемы неоднозначности данных, если состояние синглтона будет меняться, а использующие его бины, например, были или будут сериализованы.
Для создания своего скоупа нужно написать аннотацию и отметить ее @ScopeType, а также реализовать интерфейс javax.enterprise.context.spi.Context.
Небольшая путаница может возникнуть с тем, что в пакете javax.faces.bean также находятся аннотации для управления скоупом managed бинов JSF. Связано это с тем, что в JSF приложениях использование CDI не обязательно: действительно, ведь можно обойтись стандартными инъекциями с помощью @EJB, @PersistenceContext и т.п. Однако, если мы хотим использовать продвинутые штуки из DI, удобней применять аннотации из JSR-299 и 330.
Допустим есть сервис проверяющий логин и пароль пользователя.
Напишем его реализацию:
Теперь добавим контроллер, который будет использовать сервис логина:
Как видно из примера, в этом случае для инъекции с помощью Weld необходимо добавить аннотацию Inject в нужном поле: контейнер найдет все возможные реализации интерфейса, выберет подходящую и создаст объект привязанный к скоупу контроллера. Естественно поддерживаются инъекции в метод и конструктор. В примере также используется аннотация Named, она служит для того, чтобы к бину можно было обращаться в EL по имени.
В процессе разработки нам бы хотелось иметь свою реализацию сервиса, примерно такого содержания:
Теперь после редеплоя приложения в консоли возникнет ошибка:
Если в точке инъекции подходят несколько реализаций, то Weld бросает исключение. Разработчики CDI предусмотрели разрешение такой проблемы. Для этого отметим StubLoginService аннотацией Alternative:
Теперь данная реализация недоступна для инъекций и после редеплоя ошибка не возникнет, однако сейчас Weld делает не совсем то, что нам нужно. Добавим следующее в beans.xml:
<alternatives>
<class>com.sample.service.StubLoginService</class>
</alternatives>
Таким образом мы подменили реализацию при старте приложения. Чтобы вернуть рабочую версию достаточно закомментировать строчку в файле настройке бинов.
Как это обычно бывает, обновили спецификацию и теперь сервис должен проверять пароль по хешу с использованием MD5 алгоритма. Добавим еще одну реализацию:
Теперь нужно сообщить Weld, что в точке инъекции необходимо подставить именно Md5LoginService. Для этого воспользуемся qualifier annotations. Сама идея очень проста: когда контейнер решает какую именно реализацию нужно внедрить, он проверяет аннотации в точке инъекции и аннотации у возможных реализаций. Проверяемые аннотации называются спецификаторами (qualifier). Спецификатор это обычная java аннотация, которая дополнительно аннотирована javax.inject.Qualifier:
Теперь в контроллере проаннатируем поле в которое будет совершена подстановка, а также реализацию Md5LoginService:
Теперь Weld подставит нужную реализацию. Конечно возникает вопрос: кажды�� раз писать новую аннотацию?! В большинстве случаев так и придется сделать, но эта плата за typesafe. К тому же аннотации можно агрегировать, например так:
Более того, чтобы не создавать большое количество одинаковых аннотаций, можно расширять аннотации полями и Weld учтет это:
Т.е. для рассматриваемого примера мы добавили новое поле c типом перечисления, указывающий какой именно это алгоритм хеширования. Пусть и пришлось написать новую аннотацию и перечисление, зато теперь практически невозможно ошибиться с выбором нужной реализации, к тому же увеличилась читабельность кода. Также нужно всегда помнить, что если реализация не имеет спецификатора, то он добавляется по умолчанию javax.enterprise.inject.Default.
Правда после проделанных манипуляций StubLoginService перестал подставляться в поле. Это связано с тем, что у него нет спецификатора Hash, поэтому Weld даже не рассматривает его как возможную реализацию интерфейса. Для решения этой проблемы есть одна хитрость: аннотация @Specializes, которая заменяет реализацию другого бина. Чтобы указать Weld какую именно реализацию нужно заменить, нужно ее просто расширить:
Представим, что у нас появились новые требования: при попытке входа пользователя в систему, нужно попытаться проверить пароль всеми возможными алгоритмами реализованными в системе. Т.е. нам нужно перебрать все реализации интерфейса. В Spring такая задача решается через подстановку в коллекцию обобщенную по нужному интерфейсу. В Weld для этого можно использовать интерфейс javax.enterprise.inject.Instance<?> и встроенный спецификатор Any. Пока отключим альтернативные реализации и посмотрим, что получится:
Аннотация Any говорит о том, что нам все равно какие спецификаторы могут быть у реализаций. Интерфейс Instance реализует Iterable, поэтому с ним можно делать такие красивые штуки через foreach. Вообще этот интерфейс предназначен не только для этого. Он содержит перегруженный метод select(), который позволяют в runtime выбирать нужную реализацию. В качестве параметров он принимает экземпляры аннотаций. В целом сейчас это реализовано несколько “необычно”, поскольку приходится создавать анонимные классы (или создавать отдельно, только для того чтобы использовать в одном месте). Частично это решается абстрактным классом AnnotationLiteral<?> от которого можно расшириться и обобщить по нужной аннотации. Помимо этого в Instance есть специальные методы isUnsatisfied и isAmbiguous, с помощью которых можно в runtime проверить есть ли подходящая реализация и только потом получить ее экземпляр через метод get(). Выглядит это примерно так:
Понятно, что в данном случае можно было пройтись циклом по loginServiceInstance, как это сделали в примере выше, и найти нужную реализацию по getClass().equals(), но тогда при изменении реализаций пришлось править код и в этом месте тоже. Weld представляет более гибкий и безопасный подход, пусть немного добавляя новых абстракций для изучения.
Как уже было отмечено выше, Weld при выборе нужной реализации руководствуется и типом и спецификатором. Но в некоторых случаях, например со сложной иерархией наследования, мы можем указать тип реализации в ручную, используя аннотацию @Typed.
Это все конечно хорошо, но что делать когда нам нужно создать экземпляр класса каким-то хитрым способом? Опять же Spring в xml контексте предлагает богатый набор тегов для инициализации свойств у объектов, создания списков, мап и т.д. У Weld для этого есть всего одна аннотация @Produces, которой отмечаются методы и поля генерирующие объекты (в том числе и скалярных типов). Перепишем наш предыдущий пример:
Теперь укажем через спецификатор откуда хотим получить реализацию:
Вот собственно и все. Источником может быть и обычное поле. Правила подстановки теже самые. Более того в метод buildLoginService можно также инъектировать бины:
Как видите модификатор доступа никак не влияет. Скоуп объектов генерируемых buildLoginService не привязан к скоупу бина, в котором он объявлен, поэтому в данном случае он будет Dependent. Чтобы это изменить достаточно добавить аннотацию к методу, например так:
Помимо этого можно вручную освобождать ресурсы генерируемые с помощью @Produces. Для этого, Вы не поверите, есть другая аннотация @Disposed, которая работает примерно так:
Когда жизненный цикл объекта подходит к концу, Weld ищет методы удовлетворяющие типу и спецификатору метода генаратора, а также помеченные @Disposed и вызывает его.
Мы рассмотрели далеко не все возможности JSR-299. Помимо этого есть ряд дополнительных спецификаторов, механизмы управления жизненным циклом бинов внутри контейнера (interceptors, decorators), стереотипы, событийная модель, с помощью которой удобно организовывать сложную бизнес логику и еще много-много приятных мелочей.
В данной статье не хотелось противопоставлять Weld другим dependency injection фреймворкам, о которых говорилось в начале. Weld самодостаточен и обладает интересной реализацией, достойной внимания Java Enterprise разработчиков.
JSR-299
Официальная документации по Weld
Отличная вводная статья по JSR-299 от инженера Oracle
Цикл статей по поддержке CDI в NetBeans на русском языке (1, 2, 3, 4)
К сожалению в сети не так много русскоязычной информации о нем. Скорее всего это связано с тем, что Spring IOC является синонимом dependency injection в Java Enterprise приложениях. Есть конечно еще Google Guice, но он тоже не так популярен.
В статье хотелось бы рассказать об основных преимуществах и недостатках Weld.
Немного теории
В начале стоит упомянуть про JSR-330 “Dependency Injection for Java” спецификацию, разработанную инженерами из SpringSource и Google, определяющую базовые механизмы для реализации DI в Java приложениях. Как и Spring и Guice, Weld использует аннотации предусмотренные данной спецификацией.
Weld может работать не только с Java EE приложениями, но и с обычным окружением Java SE. Естественно есть поддержка Tomcat, Jetty; в официальной документации описаны подробные инструкции по настройке.
В Contexts and Dependency Injection (CDI) невозможно инъектировать бин через его имя в виде строки, как это например делается в Spring через Qualifier. Вместо этого используется шаблон qualifier annotations (о котором ниже). С точки зрения создателей CDI, это более typesafe подход, позволяющий избежать некоторых ошибок и обеспечивающий гибкость DI.
Для того, чтобы задействовать CDI нужно создать файл beans.xml в директории WEB-INF для веб-приложения (или в META-INF для Java SE):
<beans xmlns=«java.sun.com/xml/ns/javaee»
xmlns:xsi=«www.w3.org/2001/XMLSchema-instance»
xsi:schemaLocation=«java.sun.com/xml/ns/javaee java.sun.com/xml/ns/javaee/beans_1_0.xsd»>
</beans>
Как и в Spring, у бинов в контексте CDI есть свой скоуп в течении которого они существуют. Скоуп задается с помощью аннотаций из пакета javax.enterprise.context:
- @RequestScoped
- @SessionScoped
- @ApplicationScoped
- Dependent
- @ConversationScoped
@ConversationScoped представляет собой определенный промежуток времени взаимодействия пользователя с конкретной вкладкой в браузере. Поэтому он чем-то похож на жизненный цикл сессии, но важное отличие в том, что старт “сессии” задается вручную. Для этого объявлен интерфейс javax.enterprise.context.Conversation, который определяет методы start(), end(), а также setTimeout(long), для закрытия сессии по истечении времени.
Конечно же, есть и Singleton, который находится в пакете javax.inject, т.к. является частью спецификации JSR-330. В реализации CDI данного скоупа есть одна особенность: в процессе инъекции клиент получает ссылку на реальный объект созданный контейнером, а не proxy. В результате чего могут быть проблемы неоднозначности данных, если состояние синглтона будет меняться, а использующие его бины, например, были или будут сериализованы.
Для создания своего скоупа нужно написать аннотацию и отметить ее @ScopeType, а также реализовать интерфейс javax.enterprise.context.spi.Context.
Небольшая путаница может возникнуть с тем, что в пакете javax.faces.bean также находятся аннотации для управления скоупом managed бинов JSF. Связано это с тем, что в JSF приложениях использование CDI не обязательно: действительно, ведь можно обойтись стандартными инъекциями с помощью @EJB, @PersistenceContext и т.п. Однако, если мы хотим использовать продвинутые штуки из DI, удобней применять аннотации из JSR-299 и 330.
Примерчик
Допустим есть сервис проверяющий логин и пароль пользователя.
public interface ILoginService extends Serializable { boolean login(String name, String password); }
Напишем его реализацию:
public class LoginService implements ILoginService { @Override public boolean login(String name, String password) { return "bugs".equalsIgnoreCase(name) && "bunny".equalsIgnoreCase(password); } }
Теперь добавим контроллер, который будет использовать сервис логина:
@Named @RequestScoped public class LoginController { @Inject private ILoginService loginService; private String login; private String password; public String doLogin() { return loginService.login(login, password) ? "main.xhtml" : "failed.xhtml"; } // getters and setters will be omitted... }
Как видно из примера, в этом случае для инъекции с помощью Weld необходимо добавить аннотацию Inject в нужном поле: контейнер найдет все возможные реализации интерфейса, выберет подходящую и создаст объект привязанный к скоупу контроллера. Естественно поддерживаются инъекции в метод и конструктор. В примере также используется аннотация Named, она служит для того, чтобы к бину можно было обращаться в EL по имени.
В процессе разработки нам бы хотелось иметь свою реализацию сервиса, примерно такого содержания:
public class StubLoginService implements ILoginService { @Override public boolean login(String name, String password) { return true; } }
Теперь после редеплоя приложения в консоли возникнет ошибка:
WELD-001409 Ambiguous dependencies for type [ILoginService] with qualifiers [@Default] at injection point [[field] @Inject private com.sample.controller.LoginController.loginService].
Если в точке инъекции подходят несколько реализаций, то Weld бросает исключение. Разработчики CDI предусмотрели разрешение такой проблемы. Для этого отметим StubLoginService аннотацией Alternative:
@Alternative public class StubLoginService implements ILoginService { … }
Теперь данная реализация недоступна для инъекций и после редеплоя ошибка не возникнет, однако сейчас Weld делает не совсем то, что нам нужно. Добавим следующее в beans.xml:
<alternatives>
<class>com.sample.service.StubLoginService</class>
</alternatives>
Таким образом мы подменили реализацию при старте приложения. Чтобы вернуть рабочую версию достаточно закомментировать строчку в файле настройке бинов.
Как это обычно бывает, обновили спецификацию и теперь сервис должен проверять пароль по хешу с использованием MD5 алгоритма. Добавим еще одну реализацию:
public class Md5LoginService implements ILoginService { @Override public boolean login(String name, String password) { // делаем проверку... } }
Теперь нужно сообщить Weld, что в точке инъекции необходимо подставить именно Md5LoginService. Для этого воспользуемся qualifier annotations. Сама идея очень проста: когда контейнер решает какую именно реализацию нужно внедрить, он проверяет аннотации в точке инъекции и аннотации у возможных реализаций. Проверяемые аннотации называются спецификаторами (qualifier). Спецификатор это обычная java аннотация, которая дополнительно аннотирована javax.inject.Qualifier:
@Retention(RetentionPolicy.RUNTIME) @Target({FIELD, PARAMETER, TYPE, METHOD}) @Qualifier public @interface Hash { }
Теперь в контроллере проаннатируем поле в которое будет совершена подстановка, а также реализацию Md5LoginService:
@Hash public class Md5LoginService implements ILoginService { } @Named @RequestScoped public class LoginController { @Inject @Hash private ILoginService loginService; }
Теперь Weld подставит нужную реализацию. Конечно возникает вопрос: кажды�� раз писать новую аннотацию?! В большинстве случаев так и придется сделать, но эта плата за typesafe. К тому же аннотации можно агрегировать, например так:
@Hash @Fast public class Md5LoginService implements ILoginService { } @Named @RequestScoped public class LoginController { @Inject @Hash @Fast private ILoginService loginService; }
Более того, чтобы не создавать большое количество одинаковых аннотаций, можно расширять аннотации полями и Weld учтет это:
@Retention(RetentionPolicy.RUNTIME) @Target({FIELD, PARAMETER, TYPE, METHOD}) @Qualifier public @interface Hash { HashType value() default HashType.SHA; // Поля помеченные аннотацией @Nonbinding при выборе реализации учитываться не будут @Nonbinding String desc() default ""; } @Hash(HashType.MD5) public class Md5LoginService implements ILoginService { } @Named @RequestScoped public class LoginController { @Inject @Hash(HashType.MD5) private ILoginService loginService; }
Т.е. для рассматриваемого примера мы добавили новое поле c типом перечисления, указывающий какой именно это алгоритм хеширования. Пусть и пришлось написать новую аннотацию и перечисление, зато теперь практически невозможно ошибиться с выбором нужной реализации, к тому же увеличилась читабельность кода. Также нужно всегда помнить, что если реализация не имеет спецификатора, то он добавляется по умолчанию javax.enterprise.inject.Default.
Правда после проделанных манипуляций StubLoginService перестал подставляться в поле. Это связано с тем, что у него нет спецификатора Hash, поэтому Weld даже не рассматривает его как возможную реализацию интерфейса. Для решения этой проблемы есть одна хитрость: аннотация @Specializes, которая заменяет реализацию другого бина. Чтобы указать Weld какую именно реализацию нужно заменить, нужно ее просто расширить:
@Alternative @Specializes public class StubLoginService extends Md5LoginService { @Override public boolean login(String name, String password) { return true; } }
Представим, что у нас появились новые требования: при попытке входа пользователя в систему, нужно попытаться проверить пароль всеми возможными алгоритмами реализованными в системе. Т.е. нам нужно перебрать все реализации интерфейса. В Spring такая задача решается через подстановку в коллекцию обобщенную по нужному интерфейсу. В Weld для этого можно использовать интерфейс javax.enterprise.inject.Instance<?> и встроенный спецификатор Any. Пока отключим альтернативные реализации и посмотрим, что получится:
@Named @RequestScoped public class LoginController { @Inject @Any private Instance<ILoginService> loginService; private String login; private String password; public String doLogin() { for (ILoginService service : loginService) { if (service.login(login, password)) return "main.xhtml"; } return "failed.xhtml"; } }
Аннотация Any говорит о том, что нам все равно какие спецификаторы могут быть у реализаций. Интерфейс Instance реализует Iterable, поэтому с ним можно делать такие красивые штуки через foreach. Вообще этот интерфейс предназначен не только для этого. Он содержит перегруженный метод select(), который позволяют в runtime выбирать нужную реализацию. В качестве параметров он принимает экземпляры аннотаций. В целом сейчас это реализовано несколько “необычно”, поскольку приходится создавать анонимные классы (или создавать отдельно, только для того чтобы использовать в одном месте). Частично это решается абстрактным классом AnnotationLiteral<?> от которого можно расшириться и обобщить по нужной аннотации. Помимо этого в Instance есть специальные методы isUnsatisfied и isAmbiguous, с помощью которых можно в runtime проверить есть ли подходящая реализация и только потом получить ее экземпляр через метод get(). Выглядит это примерно так:
@Inject @Any Instance<ILoginService> loginServiceInstance; public String doLogin() { Instance<ILoginService> tempInstance = isUltimateVersion ? loginServiceInstance.select(new AnnotationLiteral<Hash>(){}) : loginServiceInstance.select(new AnnotationLiteral<Default>(){}); if (tempInstance.isAmbiguous() || tempInstance.isUnsatisfied()) { throw new IllegalStateException("Не могу найти подходящую реализацию"); } return tempInstance.get().login(login, password) ? "main.xhtml" : "failed.xhtml"; }
Понятно, что в данном случае можно было пройтись циклом по loginServiceInstance, как это сделали в примере выше, и найти нужную реализацию по getClass().equals(), но тогда при изменении реализаций пришлось править код и в этом месте тоже. Weld представляет более гибкий и безопасный подход, пусть немного добавляя новых абстракций для изучения.
Как уже было отмечено выше, Weld при выборе нужной реализации руководствуется и типом и спецификатором. Но в некоторых случаях, например со сложной иерархией наследования, мы можем указать тип реализации в ручную, используя аннотацию @Typed.
Это все конечно хорошо, но что делать когда нам нужно создать экземпляр класса каким-то хитрым способом? Опять же Spring в xml контексте предлагает богатый набор тегов для инициализации свойств у объектов, создания списков, мап и т.д. У Weld для этого есть всего одна аннотация @Produces, которой отмечаются методы и поля генерирующие объекты (в том числе и скалярных типов). Перепишем наш предыдущий пример:
@ApplicationScoped public class LoginServiceFactory implements Serializable { // @Factory - кастомный спецификатор @Produces @Factory public ILoginService buildLoginService() { return isUltimateVersion ? new Md5LoginService() : new LoginService(); } }
Теперь укажем через спецификатор откуда хотим получить реализацию:
@Inject @Factory private ILoginService loginService;
Вот собственно и все. Источником может быть и обычное поле. Правила подстановки теже самые. Более того в метод buildLoginService можно также инъектировать бины:
@Produces @Factory private ILoginService buildLoginService( @Hash(HashType.MD5) ILoginService md5LoginService, ILoginService defaultLoginService) { return isUltimateVersion ? md5LoginService : defaultLoginService; }
Как видите модификатор доступа никак не влияет. Скоуп объектов генерируемых buildLoginService не привязан к скоупу бина, в котором он объявлен, поэтому в данном случае он будет Dependent. Чтобы это изменить достаточно добавить аннотацию к методу, например так:
@Produces @Factory @SessionScoped private ILoginService buildLoginService( @Hash(HashType.MD5) ILoginService md5LoginService, ILoginService defaultLoginService) { return isUltimateVersion ? md5LoginService : defaultLoginService; }
Помимо этого можно вручную освобождать ресурсы генерируемые с помощью @Produces. Для этого, Вы не поверите, есть другая аннотация @Disposed, которая работает примерно так:
private void dispose(@Disposes @Factory ILoginService service) { log.info("LoginService disposed"); }
Когда жизненный цикл объекта подходит к концу, Weld ищет методы удовлетворяющие типу и спецификатору метода генаратора, а также помеченные @Disposed и вызывает его.
Заключение
Мы рассмотрели далеко не все возможности JSR-299. Помимо этого есть ряд дополнительных спецификаторов, механизмы управления жизненным циклом бинов внутри контейнера (interceptors, decorators), стереотипы, событийная модель, с помощью которой удобно организовывать сложную бизнес логику и еще много-много приятных мелочей.
В данной статье не хотелось противопоставлять Weld другим dependency injection фреймворкам, о которых говорилось в начале. Weld самодостаточен и обладает интересной реализацией, достойной внимания Java Enterprise разработчиков.
Источники
JSR-299
Официальная документации по Weld
Отличная вводная статья по JSR-299 от инженера Oracle
Цикл статей по поддержке CDI в NetBeans на русском языке (1, 2, 3, 4)