Статьи

Играть в!

В проекте, которым я сейчас работаю, мы начали использовать Google Guice. Для тех, кто не знает, Google Guice — это структура внедрения зависимостей. Основная идея внедрения зависимости состоит в том, чтобы предоставить классу его зависимости вместо того, чтобы сделать зависимый класс ответственным за создание экземпляров объектов, от которых он зависит.

В Play есть модуль для интеграции Guice:
http://www.playframework.org/modules/guice-1.2/home

В дополнение к документации модуля, этот пост от @_felipera может помочь вам начать работу.
http://geeks.aretotally.in/dependency-injection-with-play-framework-and-google-guice

Как использовать модуль Guice

Добавить зависимость

1
2
3
require:
  - play
  - play -> guice 1.2

Загрузите зависимости

1
play deps

Создайте новый класс, который будет введен в контроллер
services.MyService

1
2
3
4
package services;
public interface MyService {
   public void sayHello();
}

services.MyServiceImpl

01
02
03
04
05
06
07
08
09
10
11
package services;
public class MyServiceImpl implements MyService {
    public MyServiceImpl(){
        play.Logger.info("constructor!");
    }
 
    @Override
    public void sayHello() {
        play.Logger.info("hello");
    }
}

Настроить инжектор

01
02
03
04
05
06
07
08
09
10
11
12
package config;
public class GuiceConfig extends GuiceSupport {
    @Override
    protected Injector configure() {
        return Guice.createInjector(new AbstractModule() {
            @Override
            protected void configure() {
                bind(MyService.class).to(MyServiceImpl.class).in(Singleton.class);
            }
        });
    }
}

Это установит класс как синглтон. Каждый раз, когда у класса есть зависимость MyService, инжектор будет внедрять один и тот же экземпляр MyServiceImpl.

Вставьте зависимость, используя аннотацию @Inject

01
02
03
04
05
06
07
08
09
10
11
package controllers;
public class Application extends Controller {
 
    @Inject
    static MyService myService;
 
    public static void index() {
        myService.sayHello();
        render();
    }
}

тестирование
Следующим моим шагом было создание теста, и тут возникла первая проблема.

1
play test

Http: // Localhost: 9000 / @ тесты
Ошибка компиляции! Проблема в том, что в модуле есть папка «test». Эта папка должна иметь какой-то модульный или функциональный тест, но вместо этого она имеет три примера приложений. Соглашение в игровых модулях заключается в том, что приложения такого типа должны находиться в папке «samples-and-test».

Я сделал форк проекта, чтобы переименовать эту папку:
https://github.com/axelhzf/play-guice-module
Я также сделал pull-запрос, но пока не получил ответа:
https://github.com/pk11/play-guice-module/pull/5
Чтобы запустить этот тест, достаточно было переименовать папку «test»:

01
02
03
04
05
06
07
08
09
10
@InjectSupport
public class InjectTest extends UnitTest {
    @Inject
    static MyService myService;
 
    @Test
    public void injectOk(){
        assertNotNull(myService);
    }
}

Добавление большего количества зависимостей
По умолчанию Play автоматически обнаруживает аннотацию @Inject для классов, которые наследуются от Controller, Job и Mail. Если вы хотите внедрить зависимости в другие классы, вы должны использовать @InjectSupport.
Как правило, наши услуги не так просты, как MyService. Распространены зависимости между сервисами. Guice решает эту проблему, анализируя зависимости и создавая объекты в правильном порядке.
services.MyDependentService

1
2
3
4
5
package services;
 
public interface MyDependentService {
    public void sayHelloWorld();
}

service.MyDependentServiceImpl

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package services;
 
@InjectSupport
public class MyDependentServiceImpl implements MyDependentService {
    @Inject
    static MyService myService;
 
    public MyDependentServiceImpl(){
        play.Logger.info("Init MyDependentServiceImpl");
    }
 
    public void sayHelloWorld(){
        myService.sayHello();
        play.Logger.info("world");
    }
}

InjectTest

01
02
03
04
05
06
07
08
09
10
11
12
13
@InjectSupport
public class InjectTest extends UnitTest {
 
@Inject
static MyDependentService myDependentService;
 
@Test
public void injectOk(){
    assertNotNull(myDependentService);
    myDependentService.sayHelloWorld();
}
 
}

Связывание в GuiceConfig

1
bind(MyDependentService.class).to(MyDependentServiceImpl.class).in(Singleton.class);

И это вывод консоли

1
2
3
4
5
20:34:39,090 INFO ~ Init MyServiceImpl
20:34:39,095 INFO ~ Init MyDependentServiceImpl
20:34:39,095 INFO ~ Application 'lazySingleton' is now started !
20:34:39,136 INFO ~ hello
20:34:39,136 INFO ~ world

Конструктор инъекций
В модуле мне не нравится то, что поля, которые вам разрешено вводить, должны быть статическими. Я бы предпочел объявить зависимости как параметры конструктора. Таким образом, было бы более очевидно, что для создания экземпляра MyDependentServiceImpl вам необходим экземпляр MyService. Более того, при работе с модульным тестом легче передавать фиктивные объекты в качестве параметра, чем настраивать инжектор.
В документации к модулю я не нашел ссылки на то, как это сделать. Тем не менее, я нашел статью, объясняющую, как это сделать с помощью провайдера:
http://ericlefevre.net/wordpress/2011/05/08/play-framework-and-guice-use-providers-in-guice-modules/
Позже я нашел вопрос о StackOverflow, который дал мне другую подсказку:
http://stackoverflow.com/questions/8435686/does-injector-getinstance-always-call-a-constructor
В Edit он говорит, что забыл поместить аннотацию @Inject в конструктор. Я попытался сделать то же самое, и, наконец, это сработало:

01
02
03
04
05
06
07
08
09
10
11
public class MyDependentServiceImpl implements MyDependentService {
 
    private final MyService myService;
    @Inject
 
    public MyDependentServiceImpl(MyService myService){
        this.myService = myService;
        play.Logger.info("Inicializando MyDependentServiceImpl");
    }
 
    ...

Ленивые синглтоны
Это еще одна последняя деталь, чтобы иметь идеальную конфигурацию Google Guice.
Службы инициализируются при запуске приложения.

1
2
3
21:38:11,801 INFO ~ Inicializando MyServiceImpl
21:38:11,805 INFO ~ Inicializando MyDependentServiceImpl
21:38:11,805 INFO ~ Application 'lazySingleton' is now started !

Когда приложение находится в рабочем режиме, это правильное поведение. Но в режиме разработки я предпочитаю, чтобы синглтоны запускались по требованию. Могут быть службы, которые требуют времени для запуска, и я хочу, чтобы запуск приложения был максимально быстрым.

С Google Guice вы можете достичь этого, используя Scopes:
http://code.google.com/p/google-guice/wiki/Scopes
Все, что вам нужно сделать, это установить параметр Stage:

1
2
Stage stage = Play.mode.isDev() ? Stage.DEVELOPMENT : Stage.PRODUCTION;
return Guice.createInjector(stage, new AbstractModule() {…..

Перезапуск теста

1
2
3
4
5
6
22:00:03,353 WARN ~ You're running Play! in DEV mode
22:00:04,615 INFO ~ Connected to jdbc:h2:mem:play;MODE=MYSQL;LOCK_MODE=0
22:00:04,811 INFO ~ Guice injector created: config.GuiceConfig
22:00:04,819 INFO ~ Init MyServiceImpl
22:00:04,824 INFO ~ Init MyDependentServiceImpl
22:00:04,824 INFO ~ Application 'lazySingleton' is now started !

По электронной почте Ой! Синглтон инициализируются до запуска приложения. Возможно, это неправильное использование переменной сцены. Давайте попробуем тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class StageTest {
 
    @Test
    public void testDevelopment(){
        Injector injector = createInjector(Stage.DEVELOPMENT);
        System.out.println("development - before getInstance");
        MyService instance = injector.getInstance(MyService.class);
        System.out.println("development - after getInstance");
    }
 
    @Test
    public void testProduction(){
        Injector injector = createInjector(Stage.PRODUCTION);
        System.out.println("production - before getInstance");
        MyService instance = injector.getInstance(MyService.class);
        System.out.println("production - after getInstance");
    }
 
    public Injector createInjector(Stage stage){
        Injector injector = Guice.createInjector(stage, new AbstractModule(){
            @Override
            protected void configure() {
                bind(MyService.class).to(MyServiceImpl.class);
            }
        });
        return injector;
    }
}

И результат:

1
2
3
4
5
6
7
INFO: development - before getInstance
INFO: Inicializando MyServiceImpl
INFO: development - after getInstance
 
INFO: Inicializando MyServiceImpl
INFO: production - before getInstance
INFO: production - after getInstance

Как сказано в документации, в режиме разработки Singletons инициализируются лениво.
Если это работает, когда я пытался использовать модуль воспроизведения, почему это не сработало?

Просмотр кода:
https://github.com/pk11/play-guice-module/blob/master/src/play/modules/guice/GuicePlugin.java
@OnApplicationStart модуль находит все классы, аннотированные @InjectSupport, и внедряет их зависимости. Чтобы внедрить зависимости, модуль вызывает метод getBean (). И здесь мы находим проблему: при вызове getBean () создается экземпляр класса.

Я нашел решение этой проблемы:
https://groups.google.com/d/msg/google-guice/405HVgnCzsQ/fBUuueP6NfsJ
Это код:

Эти классы создают прокси для каждого класса, помеченного как @LazySingleton. Когда объект вводится, инжектор фактически вводит прокси. При первом вызове метода прокси позаботится об инициализации класса.

Используя эти классы, конфигурация инжектора выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GuiceConfig extends GuiceSupport {
    @Override
    protected Injector configure() {
        Stage stage = Play.mode.isDev() ? Stage.DEVELOPMENT : Stage.PRODUCTION;
        return Guice.createInjector(stage, new AbstractModule() {
            @Override
            protected void configure() {
                bindScope(LazySingleton.class, MoreScopes.LAZY_SINGLETON);
                bindLazySingletonOnDev(MyService.class, MyServiceImpl.class);
                bindLazySingletonOnDev(MyDependentService.class, MyDependentServiceImpl.class);
            }
 
            protected <T> void bindLazySingletonOnDev(Class<T> expected, Class<? extends T> implClass){
                if(Play.mode.isDev()){
                    bind(implClass).in(MoreScopes.LAZY_SINGLETON);
                    Provider<T> provider = LazyBinder.newLazyProvider(expected, implClass);
                    bind(expected).toProvider(provider);
                }else{
                    bind(expected).to(implClass).in(Scopes.SINGLETON);
                }
            }
        });
    }
}

Я добавлю эти классы на развилку, чтобы иметь полный модуль, который можно повторно использовать в разных проектах.

Вывод
За последние несколько лет Dependency Injection превратилась из неясного и мало понятного термина в часть инструментария каждого программиста. В этой статье мы увидели, как легко интегрировать Guice, очень удобную библиотеку от Google, в приложение Play Framework. Кроме того, мы также рассмотрели, как настроить его поведение для лучшего опыта разработки.

Оригинал статьи опубликован на http://axelhzf.tumblr.com .

Ссылка: Playframework + Google Guice от нашего партнера JCG   блог.