Pull to refresh

Dependency injection в Java EE 6

Reading time 9 min
Views 97K
В рамках 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:
  • @RequestScoped
  • @SessionScoped
  • @ApplicationScoped
  • Dependent
  • @ConversationScoped
С первыми тремя скоупами все ясно. Используемый по умолчанию в CDI, Dependent привязывается непосредственно к бину клиента и существует все время пока живет “родитель”.

@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)
Tags:
Hubs:
+18
Comments 6
Comments Comments 6

Articles