В рамках 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)