воскресенье, 6 апреля 2008 г.

Знакомимся: Guice - IoC контейнер от Google


Перед любым проектировщиком и/или разработчиком более-менее сложного приложения встает вопрос: "Как уменьшить связность кода?" Собственно, уже давно ни для кого не секрет, что излишняя связность кода приложения - верный путь к увеличению энтропии и сложности поддержки. Особенно, если код криво написан, что, к сожалению, не редкость.

Одним из наиболее популярных (на мой взгляд - лучших) способов уменьшения связности кода является использование паттерна Inversion Of Control (Инверсия управления). Очень часто данный паттерн описывают основным правилом Голливуда: "не звони мне, я сам тебе позвоню". При всей философичности данного описания оно является правильным.

Без философии же данный паттерн описывается следующим образом: взаимодействовать должны не реализации (классы), а абстракции (интерфейсы). Не должно быть такого (без фанатизма конечно же), когда класс зависит от класса. Классы должны зависить от интерфейсов. Возникает вопрос - когда создавать инстансы интерфейсов, ведь понятно, что в рантайме интерфейсов не будет, а будут классы. Так вот, вся прелесть IoC проявляется именно в этом. Вы в одном месте описываете правила, по которым будут созданы объекты для реализации интерфейсов. Остальное за вас сделает используемая IoC-подсистема, в частности - IoC-контейнер.


В мире Java для реализации паттерна IoC очень часто используются специальные средства, именуемые
IoC-контейнерами.

Идея состоит в следующем: все интерфейсы инстанцируются специальной фабрикой (контейнером) по некоторому описанию. Описание, собственно, и указывает контейнеру какой интерфейс, в каком случае, каким классом инстанцировать.

Наиболее популярными IoC контейнерами в мире Java являются Spring Framework, HiveMind, Tapestry-IoC (следующая реинкарнация HiveMind), Pico и другие. Но мы сегодня поговорим о Guice - легковесном IoC-контейнере от Google.

Скачать Guice можно как не трудно догадаться с code.google.com. Текущая версия - 1.0. На сайте проекта так-же присутствует ссылка на документацию (в google-doc) но я рекомендую сразу качать pdf - там информация более полная.

Сначала я расскажу о том, что понравилось в Guice, потом кратко покажу основные приемы работы.

Итак, понравилось в Guice:

1. Отсутствие xml-конфигов. Очень напрягает читать портянки конфигов, да еще и разбитые по нескольким файлам (зачастую даже без всякой логики). Очень сильно грешил этим Spring до версии 2.5. В Guice все конфигурируется с помощью аннотаций в Java-коде. Места инъекций (Guice реализует разновидность IoC, которая называется Dependency Injection - инъекция зависимостей) отмечаются специальной аннотацией - @Inject. Можно также делать именованные инъекции и инъекции с помощью своих аннотаций. Далее вся конфигурация реализуется в одном классе вызовом специальных методов.

2. Модульность. С помощью Guice легко построить приложение состоящее из нескольких модулей (я сейчас работаю над реализацией плагинной шины в основе которой лежит Guice). Модуль с точки зрения Guice - отдельный класс, описывающий конфигурацию. Соответственно, можно создавать наборы модулей, заменять одни модули другими и делать все это в рантайме.

3. Широкое использование аннотаций. Можно создавать свои аннотации, аннотации с параметрами, использовать встроенную аннотацию @Named("param") для описания точек расширения. Соответственно, такой код гораздо читабельнее, нежели xml-ная портянка.

4. Инъекцию можно осуществлять на уровне конструктора, метода или непосредственно поля. Причем, в отличие от Spring метод не обязательно должен называться setParam. Он может называться как угодно, но должен быть аннотирован @Inject.

5. Можно использовать свои фабрики для создания объектов, которые будут инъектироваться. В терминах Guice такая фабрика называется провайдером. В частности, в документации по Guice приведен пример создания классов с использованием JNDI.

6. Guice - довольно шустрая штука.

Теперь попробуем сделать что-нибудь простое с помощью Guice. Пусть у нас будет простая задачка - написать приложение, копирующее строку на экран.

Итак, мы напишем класс "читатель", который будет возвращать нам данные. Естественно, он будет не просто так, а реализовывать соответствующий интерфейс. Мы же хотим получать данные по-разному. Точно также напишем класс "писатель", который будет выводить данные на экран. В копировщике же у нас в полном соответствии паттерну IoC будут взаимодействовать не "читатель" с "писателем", а интерфейс "читателя" и интерфейс "писателя". Итак, взглянем на код:

Интерфейс читателя:

package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface IReader {

    public String read();

}


Примитивный читатель:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IReader;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleReader implements IReader {



    public static final String SIMPLE_STRING = "SimpleString";

   

    public String read() {

        return SIMPLE_STRING;

    }   

}

 


Интерфейс писателя:

package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface IWriter {

    public void write(String data);

}

 


Примитивный писатель:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IWriter;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleWriter implements IWriter {



    public void write(String data) {

        System.out.println(data);

    }

}


Интерфейс копировщика:

package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface ICopier {

    public void copy();

}


Реализация копировщика:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.ICopier;

import org.google.guice.demo.modules.copier.IReader;

import org.google.guice.demo.modules.copier.IWriter;



import com.google.inject.Inject;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleCopier implements ICopier {



    private IReader reader;

    private IWriter writer;

   

    @Inject

    public SimpleCopier(IReader reader, IWriter writer) {

        this.reader = reader;

        this.writer = writer;

    }

       

    public void copy() {

        writer.write(reader.read());

    }

}

 



В коде копировщика мы видим перед конструктором аннотацию @Inject. Она уведомляет Guice о том, где именно будет происходить инъекция объектов. В данном случае при создании экземпляра класса SimpleCopier ему в конструктор будут переданы построенные читатель и писатель. Если бы мы использовали инъекцию через метод - вызывался бы метод, отмеченный аннотацией @Inject. Выглядело бы это так:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.ICopier;

import org.google.guice.demo.modules.copier.IReader;

import org.google.guice.demo.modules.copier.IWriter;



import com.google.inject.Inject;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleCopier implements ICopier {



    private IReader reader;

    private IWriter writer;

   

    public void copy() {

        writer.write(reader.read());

    }

   

    @Inject

    public void setReader(IReader reader) {

        this.reader = reader;

    }

   

    @Inject

    public void injectWriter(IWriter writer) {

        this.writer = writer;

    }

}


Как видим - нет никаких ограничений на наименования методов.

Но это еще не все. Необходимо создать модуль - класс, который будет производить конфигурацию Guice. Код модуля в данном случае очень прост - каждому интерфейсу по имени ставится в соответствие класс:

package org.google.guice.demo.modules.copier;



import org.google.guice.demo.modules.copier.impl.SimpleCopier;

import org.google.guice.demo.modules.copier.impl.SimpleReader;

import org.google.guice.demo.modules.copier.impl.SimpleWriter;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class CopyModule extends AbstractModule {



    @Override

    protected void configure() {

        bind(IReader.class)

            .to(SimpleReader.class)

            .in(Scopes.SINGLETON);



        bind(IWriter.class)

            .to(SimpleWriter.class)

            .in(Scopes.SINGLETON);

       

        bind(ICopier.class)

            .to(SimpleCopier.class)

            .in(Scopes.SINGLETON);

    }

}


Ну и наконец - метод main, который все это дело запустит:

package org.google.guice.demo;



import org.google.guice.demo.modules.copier.CopyModule;

import org.google.guice.demo.modules.copier.ICopier;



import com.google.inject.Guice;

import com.google.inject.Injector;



/**

 * <p>Created by 01.04.2008

 *   @author Samolisov Pavel

 */


public class Main {

    public static void main(String[] args) {

        Injector injector = Guice.createInjector(new CopyModule());

        ICopier service = injector.getInstance(ICopier.class);     

       

        service.copy();    

    }

}

 


Все довольно просто: получили модуль и вызвали из него инстанс для нужного интерфейса. Запустили. И ненадо писать много кода для создания классов, от которых зависит данный. Всю работу за нас делает Guice.

Пойдем дальше. Инстанцирование по типу интерфейса - это хорошо, но что делать, когда нам необходимо в разных местах модуля использовать разные реализации интерфейса? Здесь нам на помощь приходят аннотации. Причем, можно использовать стандартную, входящую в Guice аннотацию @Named с параметром. А можно написать свои аннотации. Каждый способ имеет право на жизнь: свои аннотации читать понятнее, в случае же @Named легко забыть какое значение параметра за что отвечает. Опять же, когда много аннотаций - их много надо писать )) В общем, дилема. Я покажу простой пример - пусть писателя в наш копир мы будет инъектировать с помощью своей аннотации @Writer. Напишем сначала саму аннотацию:

package org.google.guice.demo.modules.copier.annotations;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



import com.google.inject.BindingAnnotation;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */




@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.FIELD, ElementType.PARAMETER})

@BindingAnnotation

public @interface Writer {}

 


Обратите внимание: аннотации при инъекции можно навешивать только на поля или на параметры метода. Это очень важно! Написать то мы можем любую аннотацию, но если повесить ее на метод или, что еще хуже, конструктор - получим эксепшн при работе контейнера.

Ну а реализация аннотации как видим проста - все дело в волшебных пузырьках, ака @BindingAnnotation.

Изменения в коде класса SimpleCopier небольшие:

    @Inject 

    public void injectWriter(@Writer IWriter writer) {

        this.writer = writer;

    }


Самое главное - изменения в конфигурации (класс CopyModule):

bind(IWriter.class)

    .annotatedWith(Writer.class)

    .to(SimpleWriter.class)

    .in(Scopes.SINGLETON);

 


Теперь попробуем все то же самое сделать, используя стандартную для Guice аннотацию @Named

Изменения в SimpleCopier:

    @Inject 

    public void injectWriter(@Named("writer") IWriter writer) {    

        this.writer = writer;

    }


Изменения в CopyModule:

    bind(IWriter.class)

        .annotatedWith(Names.named("writer"))

        .to(SimpleWriter.class)

        .in(Scopes.SINGLETON);

 


Точка расширения, отмеченная аннотацией @Named, попадает в реестр Names. Данный реестр, как мы видим, используется в модуле при создании конфигурации.

Еще одной приятной особенностью Guice является возможность инъекции констант. Для примера - будем передавать читателю строку, которую он должен возвращать, в виде константы. Доработаем читателя:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IReader;



import com.google.inject.Inject;

import com.google.inject.name.Named;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleReader implements IReader {

   

    @Inject

    @Named("message")

    private String message;

   

    public String read() {

        return message;

    }   

}


И добавим такие строки в конфигурацию модуля:

bindConstant()

        .annotatedWith(Names.named("message"))

        .to("Hi! I'm annotated message!");


Очень удобная возможность. Позволяет легко выносить константы в файл конфигурации модуля. Вся конфигурация отныне в одном месте.

Думаю, для знакомства достаточно. Буду очень рад, если поделитесь своими впечатлениями от статьи и от Guice в комментариях. Также здорово, если помог кому-то стать на путь истинный. Помните, чем меньше связность кода - тем меньше работы по его поддержке. Используйте современные архитектурные паттерны и будет вам счастье. Всем спасибо за внимание и безбажных вам проектов.

Понравилось сообщение - подпишись на блог

25 комментариев:

Анонимный комментирует...

Спасибо Паша за статью, редко встретишь человека способного на пальцах рассказать или поделиться о чем то новом без выпендрежа

intr13 комментирует...

1. Как вы не любите портянки xml конфигов:) Наверно есть и субъективная причина;) Кстати, порой с ними жить проще, например ими удобно описывать конфигурацию транзакций в спринге.
2. Наверно стоит упомянуть что @Writer это маркер анотация. По аналогии с шаблоном программирования - маркер интерфейс. И нужен лишь для того, чтобы пометить метод (для определения какой объект в него передавать).

Unknown комментирует...

Отвечу по пунктам.
1. Конечно субьективно, ведь на то и блог. А насчет спринг-транзакций - их тоже можно определять аннотациями, недавно, кстати, написал об этом статью. ИМХО все же аннотированный код и читается легче, чем xml и писать его проще + ошибки проверяются на этапе кодинга/компиляции, а не в рантайме. Хотя, справедливости ради, скажу, что для xml тоже есть тулзы, но все же.

Да и не аннотации - основное преимущество guice (они и в Spring есть), а легковестность.

2. Я не думаю, что это - принципиальный вопрос. Аннотация она и есть аннотация - ее задача отметить метод/переменную/класс, чтобы указать на какую-то их "избранность" для чего-то.

intr13 комментирует...

Каждому свое:)

Кстати вопрос в о guice: как там с разрешением циклических зависимостей? Когда у первого класса есть ссылка на второй класс и наоборот. Мне рассказывали что в guice это сложно реализуемо.

Unknown комментирует...

Если честно - пока с такими зависимостями не сталкивался, поэтому точно сказать не могу. Но кажется здесь все фигово.

Анонимный комментирует...

Привет, Павел! Прежде всего, спасибо за труд!

У меня вопрос, как к сведущему в Guice ;)

Есть родительский класс:

public class A {
public String name;
public void setName(String name) {
this.name = name;
}


И дочерний:

public class B extends A {
}


Вопрос: как заинжектить в класс A и B разные значения использую Guice? (лично мне что-то в голову не приходит как это сделать не переопределяя метод setName() в B)

С уважением, Руслан

Unknown комментирует...

Можно попробовать сделать так:

К сожалению, константа распространяется на все классы, в которых есть одинаковые точки инжекции (в данном случае - @Named("message"))

Т.е. придется делать так:

public class A {
...
public void setName(@Named("A") String name) {
this.name = name;
}
}

public class B extends A {
...
public void setName(@Named("B") String name) {
super.setName(name);
}
}
А при определении модуля:
bindConstant() .annotatedWith(Names.named("A"))
.to("Hi! I'm A");

bindConstant() .annotatedWith(Names.named("B"))
.to("Hi! I'm B");

Если наследоваться от А очень не хочется - можно сделать хак - использовать не константу, а некий, класс, который возвращает строку, а класс биндить так:

bind(A.class) .annotatedWith(Names.named("source"))
.to(ASource.class)

bind(B.class) .annotatedWith(Names.named("source"))
.to(BSource.class)

Возвращаемые строки в ASource и BSource можно забиндить как константы (см. выше).

Анонимный комментирует...

Извини, но с последним я как то совсем не понял. Под ASource провайдер понимается?

Акцент вопроса как раз в том, можно ли в процессе инъекции использовать непосредственно "бины", а не только биндя :) интерфейсы к имплементации. (Т.е. в том примере не важно String или Foo инжектить).

Воот )

Unknown комментирует...

ASource это нечто вроде

public interface ISource
{
public String getStr();
}

public class ASource implements ISource
{
@Override
public String getStr()
{
return "I'm ASource";
}
}

Т.е. бин, который возвращает строку.

Вопрос понял. Т.е. ты хочешь спросить можно ли в одну точку расширения, определенную в разных классах инъектить разные значения. Т.е. разные объекты одного и того же класса. Похоже, что нет - нельзя. Можно инъектить только разные реализации - разные классы.

Анонимный комментирует...

Спасибо, за ответ и оперативность ;)

Unknown комментирует...

Может быть в Guice 2.0 добавили такую фичу, но я пока не разбирался.

Анонимный комментирует...

На самом деле это довольно серьезное ограничение(ситуация довольно стандартная), так что это большой минус Guice. Как бы банально не звучало, но xml от Spring невилирует такие проблемы "на ура".

Постараюсь покопаться в 2.0. Если не забуду, ответ отпишу.

Unknown комментирует...

Я не думаю, что это - заслуга XML. Просто мне не понятно, почему вместо

bindConstant() .annotatedWith(Names.named("B"))
.to("Hi! I'm B");

нельзя было сделать

bindConstant(ClassB.class) .annotatedWith(Names.named("B"))
.to("Hi! I'm B");

Анонимный комментирует...

Ну видимо они проводят аналогию с именованными константами в классах

public static final String B = "I am B!";

Ну и соответственно @Named("B") и будет являться этой константой, а уж если нужно другой значение, то будь любезен создать другую.

Я так себе это объясняю.

Мне не понятно, почему в цепочку вызовов биндинга, нигде нельзя упомянуть класс который запрашивает сервис/интерфейс. Вот тогда было бы все в шоколаде )

Unknown комментирует...

Я примерно про это и написал. Просто у них есть 3 варианта: можно забиндить константу, класс и провайдер. Класс и провайдер можно забиндить в конкретный класс, а константу - нет. Но, фактически, единственный способ непосредственно в мэппинге указать значение - только константа (в терминах Guice). Отсюда и проблемы.

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

Анонимный комментирует...

Ну провайдер это же просто фабрика, грубо говоря это возможность инстанциировать объекты самостоятельно(то что guice делает по-умолчанию), наверно, с добавлением какой-то логики.

Твоя идея, это из провайдера как то получить информацию о том "кто просит", так?

Unknown комментирует...

Получать информацию "кто просит" - поставить крест на всей идее IoC.

Только если очень очень косвенно, но красивое решение я сходу придумать не могу.

Впрочем, т.к. мы инстанцируем провайдер непосредственно в мэппинге, то никто не мешает передать в него на этом этапе нужные объекты из которых он потом будет строить то, что нам надо. Но это - грязное решение.

Анонимный комментирует...

Если интересно, вот результат исследования поднятого вопроса )

Проблема решается использованием PrivateModule

Выглядеть это должно примерно так

Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(Strng.class).toInstance("I'm A");
}
},
new PrivateModule() {
@Override
protected void configure() {
bind(B.class);
expose(B.class);
bind(String.class).to("I'm B");
}
}
);

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

Ссылки по теме:
http://pastie.org/368348
http://code.google.com/p/google-guice/wiki/FrequentlyAskedQuestions ;)

Unknown комментирует...

Спасибо за комментарий.

Но. Сейчас посмотрел - в Guice 1.0 нет PrivateModule ;)

А вообще, судя по их FAQ - можно найти более-менее красивое решение, но, как я понял, для Guice 2.0

Виталий комментирует...

Павел,
А как получить два экземпляра одного и того же класса, но с разными зависимостями?
К примеру есть:
public Interface Hello{
public void hello();
}

public class HelloMedved implements Hello{
public void hello(){
System.out.println("Hello Medved");
}
}

public class HelloWorld implements Hello{
public void hello(){
System.out.println("Hello World");
}
}

public class Printer{

private Hello hello;

@Inject
public Printer(Hello hello){
this.hello = hello;
}
}

Нужно получить два экземпляра класса Printer и чтобы guice внедрил в них разные зависимости.

Unknown комментирует...

Я сходу не могу ответить на этот вопрос, сам задумывался. Возможно проблема как-то решена в Guice 2.0, однако с ним я еще не работал.

Виталий комментирует...

Это тоже довольно стандартная ситуация. Если узнаете, напишите пож-та на почту или тут.

Виталий комментирует...

Проблема называется "robot legs" и решается с использованием тех же private modules

Ilja комментирует...

Наткнулся на статью в Гугле при поиске ответов на свои вопросы. Должен сказать, статья открывает глаза на использование IoC паттерна и "наставляет на путь истинный". Так же искал примеры для Guice и тут нашёл и то, и другое. Так что спасибо автору как за теоретическую часть, так и за практическую!

Unknown комментирует...

Спасибо. Мне приятно читать такие отзывы.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!