Перейти к основному содержимому

Guice против Spring — внедрение зависимостей

· 12 мин. чтения

1. Введение

Google Guice и Spring — две надежные платформы, используемые для внедрения зависимостей. Оба фреймворка охватывают все понятия внедрения зависимостей, но у каждого есть свой способ их реализации.

В этом руководстве мы обсудим, чем среды Guice и Spring отличаются по конфигурации и реализации.

2. Зависимости Maven

Начнем с добавления зависимостей Guice и Spring Maven в наш файл pom.xml :

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>

<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.2.2</version>
</dependency>

Мы всегда можем получить доступ к последним зависимостям spring-context или guice от Maven Central.

3. Конфигурация внедрения зависимостей

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

В этом разделе мы обратимся к нескольким основным функциям, которые различаются между Spring и Guice способами настройки внедрения зависимостей.

3.1. Весенняя проводка

Spring объявляет конфигурации внедрения зависимостей в специальном классе конфигурации. Этот класс должен быть аннотирован аннотацией @Configuration . Контейнер Spring использует этот класс в качестве источника определений bean-компонентов.

Классы, управляемые Spring, называются Spring bean-компонентами .

Spring использует аннотацию @Autowired для автоматического связывания зависимостей . @Autowired является частью встроенных основных аннотаций Spring . Мы можем использовать @Autowired для переменных-членов, методов установки и конструкторов.

Spring также поддерживает @Inject. @Inject является частью Java CDI (Contexts and Dependency Injection) , который определяет стандарт внедрения зависимостей.

Допустим, мы хотим автоматически связать зависимость с переменной-членом. Мы можем просто аннотировать его с помощью @Autowired :

@Component
public class UserService {
@Autowired
private AccountService accountService;
}
@Component
public class AccountServiceImpl implements AccountService {
}

Во-вторых, давайте создадим класс конфигурации для использования в качестве источника bean-компонентов при загрузке контекста нашего приложения:

@Configuration
@ComponentScan("com.foreach.di.spring")
public class SpringMainConfig {
}

Обратите внимание, что мы также аннотировали UserService и AccountServiceImpl с помощью @Component , чтобы зарегистрировать их как bean-компоненты. Это аннотация @ComponentScan , которая сообщает Spring, где искать аннотированные компоненты.

Несмотря на то, что мы аннотировали AccountServiceImpl , Spring может сопоставить его с AccountService , поскольку он реализует AccountService .

Затем нам нужно определить контекст приложения для доступа к bean-компонентам. Отметим, что мы будем ссылаться на этот контекст во всех наших модульных тестах Spring:

ApplicationContext context = new AnnotationConfigApplicationContext(SpringMainConfig.class);

Теперь во время выполнения мы можем получить экземпляр A ccountService из нашего bean-компонента UserService :

UserService userService = context.getBean(UserService.class);
assertNotNull(userService.getAccountService());

3.2. привязка

Guice управляет своими зависимостями в специальном классе, называемом модулем. Модуль Guice должен расширять класс AbstractModule и переопределять его метод configure() .

Guice использует привязку как эквивалент связывания в Spring. Проще говоря, привязки позволяют нам определить, как зависимости будут внедряться в класс . Привязки Guice объявляются в методе configure() нашего модуля.

Вместо @Autowired Guice использует аннотацию @Inject для внедрения зависимостей.

Давайте создадим эквивалентный пример Guice:

public class GuiceUserService {
@Inject
private AccountService accountService;
}

Во-вторых, мы создадим класс модуля, который является источником наших определений привязки:

public class GuiceModule extends AbstractModule {
@Override
protected void configure() {
bind(AccountService.class).to(AccountServiceImpl.class);
}
}

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

Затем нам нужно определить Injector с помощью GuiceModule для получения экземпляров наших классов. Отметим только, что все наши тесты Guice будут использовать этот Injector :

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

Наконец, во время выполнения мы получаем экземпляр GuiceUserService с ненулевой зависимостью accountService :

GuiceUserService guiceUserService = injector.getInstance(GuiceUserService.class);
assertNotNull(guiceUserService.getAccountService());

3.3. Аннотация Spring @Bean

Spring также предоставляет аннотацию уровня метода @Bean для регистрации bean-компонентов в качестве альтернативы аннотациям уровня класса, таким как @Component . Возвращаемое значение аннотированного метода @Bean регистрируется как компонент в контейнере.

Допустим, у нас есть экземпляр BookServiceImpl , который мы хотим сделать доступным для внедрения. Мы могли бы использовать @Bean для регистрации нашего экземпляра:

@Bean 
public BookService bookServiceGenerator() {
return new BookServiceImpl();
}

И теперь мы можем получить bean-компонент BookService :

BookService bookService = context.getBean(BookService.class);
assertNotNull(bookService);

3.4. Аннотация @Provides от Guice

Как эквивалент аннотации Spring @Bean , Guice имеет встроенную аннотацию @Provides для выполнения той же работы . Как и @Bean , @Provides применяется только к методам.

Теперь давайте реализуем предыдущий пример компонента Spring с помощью Guice. Все, что нам нужно сделать, это добавить следующий код в класс нашего модуля:

@Provides
public BookService bookServiceGenerator() {
return new BookServiceImpl();
}

И теперь мы можем получить экземпляр BookService :

BookService bookService = injector.getInstance(BookService.class);
assertNotNull(bookService);

3.5. Сканирование компонентов Classpath в Spring

Spring предоставляет аннотацию @ComponentScan , которая автоматически обнаруживает и создает экземпляры аннотированных компонентов путем сканирования предварительно определенных пакетов.

Аннотация @ComponentScan сообщает Spring, какие пакеты будут сканироваться на наличие аннотированных компонентов. Используется с аннотацией @Configuration .

3.6. Сканирование компонентов Classpath в Guice

В отличие от Spring, в Guice нет такой функции сканирования компонентов . Но смоделировать его не сложно. Есть некоторые плагины, такие как Governator , которые могут добавить эту функцию в Guice.

3.7. Распознавание объектов в Spring

Spring распознает объекты по их именам. Spring хранит объекты в структуре, примерно похожей на Map<String, Object> . Это означает, что у нас не может быть двух объектов с одинаковым именем.

Столкновение бинов из-за наличия нескольких бинов с одинаковым именем — одна из распространенных проблем, с которой сталкиваются разработчики Spring. Например, давайте рассмотрим следующие объявления компонентов:

@Configuration
@Import({SpringBeansConfig.class})
@ComponentScan("com.foreach.di.spring")
public class SpringMainConfig {
@Bean
public BookService bookServiceGenerator() {
return new BookServiceImpl();
}
}
@Configuration
public class SpringBeansConfig {
@Bean
public AudioBookService bookServiceGenerator() {
return new AudioBookServiceImpl();
}
}

Как мы помним, у нас уже было определение бина для BookService в классе SpringMainConfig .

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

Теперь давайте направим эти bean-компоненты в модульный тест:

BookService bookService = context.getBean(BookService.class);
assertNotNull(bookService);
AudioBookService audioBookService = context.getBean(AudioBookService.class);
assertNotNull(audioBookService);

Модульный тест завершится ошибкой:

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'AudioBookService' available

Во-первых, Spring зарегистрировал bean- компонент AudioBookService с именем «bookServiceGenerator» в своей карте bean-компонента. Затем ему пришлось переопределить его определением bean-компонента для BookService из-за того, что структура данных HashMap «не допускает повторяющихся имен» . ``

Наконец, мы можем решить эту проблему, сделав имена методов bean-компонентов уникальными или установив для атрибута name уникальное имя для каждого @Bean .

3.8. Распознавание объектов в Guice

В отличие от Spring, Guice в основном имеет структуру Map <Class<?>, Object> . Это означает, что мы не можем иметь несколько привязок к одному и тому же типу без использования дополнительных метаданных.

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

public class Person {
}

Теперь давайте объявим две разные привязки для класса Person :

bind(Person.class).toConstructor(Person.class.getConstructor());
bind(Person.class).toProvider(new Provider<Person>() {
public Person get() {
Person p = new Person();
return p;
}
});

А вот как мы можем получить экземпляр класса Person :

Person person = injector.getInstance(Person.class);
assertNotNull(person);

Это не удастся с:

com.google.inject.CreationException: A binding to Person was already configured at GuiceModule.configure()

Мы можем решить эту проблему, просто отбросив одну из привязок для класса Person .

3.9. Необязательные зависимости в Spring

Необязательные зависимости — это зависимости, которые не требуются при автоматическом связывании или внедрении bean-компонентов.

Для поля, которое было аннотировано с помощью @Autowired , если компонент с соответствующим типом данных не найден в контексте, Spring выдаст NoSuchBeanDefinitionException .

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

Теперь давайте посмотрим на следующий пример:

@Component
public class BookServiceImpl implements BookService {
@Autowired
private AuthorService authorService;
}
public class AuthorServiceImpl implements AuthorService {
}

Как видно из приведенного выше кода, класс AuthorServiceImpl не был аннотирован как компонент. И мы предположим, что в наших конфигурационных файлах для него нет метода объявления bean-компонента.

Теперь давайте запустим следующий тест, чтобы увидеть, что происходит:

BookService bookService = context.getBean(BookService.class);
assertNotNull(bookService);

Неудивительно, что он потерпит неудачу с:

org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type 'AuthorService' available

Мы можем сделать зависимость authorService необязательной, используя необязательный тип Java 8, `` чтобы избежать этого исключения.

public class BookServiceImpl implements BookService {
@Autowired
private Optional<AuthorService> authorService;
}

Теперь наша зависимость authorService больше похожа на контейнер, который может содержать или не содержать bean-компонент типа AuthorService . Несмотря на то, что в контексте нашего приложения нет bean-компонента для AuthorService , наше поле authorService по- прежнему будет ненулевым пустым контейнером. Следовательно, у Spring не будет причин выбрасывать NoSuchBeanDefinitionException .

В качестве альтернативы Optional мы можем использовать обязательный атрибут @Autowired , для которого по умолчанию установлено значение true , чтобы сделать зависимость необязательной. Мы можем установить для атрибута required значение false , чтобы сделать зависимость необязательной для автоматического связывания.

Следовательно, Spring пропустит внедрение зависимости, если bean-компонент для его типа данных недоступен в контексте. Зависимость останется нулевой:

@Component
public class BookServiceImpl implements BookService {
@Autowired(required = false)
private AuthorService authorService;
}

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

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

3.10. Необязательные зависимости в Guice

Как и Spring , Guice также может использовать тип Optional Java 8, чтобы сделать зависимость необязательной.

Допустим, мы хотим создать класс и с зависимостью Foo :

public class FooProcessor {
@Inject
private Foo foo;
}

Теперь давайте определим привязку для класса Foo :

bind(Foo.class).toProvider(new Provider<Foo>() {
public Foo get() {
return null;
}
});

Теперь попробуем получить экземпляр FooProcessor в модульном тесте:

FooProcessor fooProcessor = injector.getInstance(FooProcessor.class);
assertNotNull(fooProcessor);

Наш модульный тест завершится ошибкой:

com.google.inject.ProvisionException:
null returned by binding at GuiceModule.configure(..)
but the 1st parameter of FooProcessor.[...] is not @Nullable

Чтобы пропустить это исключение, мы можем сделать зависимость foo необязательной с помощью простого обновления:

public class FooProcessor {
@Inject
private Optional<Foo> foo;
}

@Inject не имеет обязательного атрибута, чтобы пометить зависимость как необязательную. Альтернативный подход к тому , чтобы сделать зависимость необязательной в Guice, — использовать аннотацию @Nullable .

Guice допускает вставку нулевых значений в случае использования @Nullable , как указано в сообщении об исключении выше. Применим аннотацию @Nullable :

public class FooProcessor {
@Inject
@Nullable
private Foo foo;
}

4. Реализации типов внедрения зависимостей

В этом разделе мы рассмотрим типы внедрения зависимостей и сравним реализации, предоставляемые Spring и Guice, на нескольких примерах.

4.1. Внедрение конструктора весной

При внедрении зависимостей на основе конструктора мы передаем необходимые зависимости в класс во время создания экземпляра.

Допустим, мы хотим иметь компонент Spring и хотим добавить зависимости через его конструктор. Мы можем аннотировать этот конструктор с помощью @Autowired :

@Component
public class SpringPersonService {

private PersonDao personDao;

@Autowired
public SpringPersonService(PersonDao personDao) {
this.personDao = personDao;
}
}

Начиная с Spring 4 зависимость @Autowired не требуется для этого типа внедрения, если класс имеет только один конструктор.

Давайте получим bean-компонент SpringPersonService в тесте:

SpringPersonService personService = context.getBean(SpringPersonService.class);
assertNotNull(personService);

4.2. Внедрение конструктора в Guice

Мы можем изменить предыдущий пример, чтобы реализовать внедрение конструктора в Guice . Обратите внимание, что Guice использует @Inject вместо @Autowired .

public class GuicePersonService {

private PersonDao personDao;

@Inject
public GuicePersonService(PersonDao personDao) {
this.personDao = personDao;
}
}

Вот как мы можем получить экземпляр класса GuicePersonService из инжектора в тесте:

GuicePersonService personService = injector.getInstance(GuicePersonService.class);
assertNotNull(personService);

4.3. Сеттер или внедрение метода в Spring

При внедрении зависимостей на основе установки контейнер будет вызывать методы установки класса после вызова конструктора для создания экземпляра компонента.

Допустим, мы хотим, чтобы Spring автоматически подключал зависимость, используя метод установки. Мы можем аннотировать этот метод установки с помощью @Autowired :

@Component
public class SpringPersonService {

private PersonDao personDao;

@Autowired
public void setPersonDao(PersonDao personDao) {
this.personDao = personDao;
}
}

Всякий раз, когда нам нужен экземпляр класса SpringPersonService , Spring автоматически связывает поле personDao , вызывая метод setPersonDao() .

Мы можем получить bean-компонент SpringPersonService и получить доступ к его полю personDao в тесте, как показано ниже:

SpringPersonService personService = context.getBean(SpringPersonService.class);
assertNotNull(personService);
assertNotNull(personService.getPersonDao());

4.4. Сеттер или внедрение метода в Guice

Мы просто немного изменим наш пример, чтобы добиться внедрения сеттера в Guice .

public class GuicePersonService {

private PersonDao personDao;

@Inject
public void setPersonDao(PersonDao personDao) {
this.personDao = personDao;
}
}

Каждый раз, когда мы получаем экземпляр класса GuicePersonService из инжектора, мы будем передавать поле personDao методу установки выше.

Вот как мы можем создать экземпляр класса GuicePersonService и получить доступ к его полю personDao `` в тесте:

GuicePersonService personService = injector.getInstance(GuicePersonService.class);
assertNotNull(personService);
assertNotNull(personService.getPersonDao());

4.5. Полевая инъекция весной

Мы уже видели, как применять внедрение полей как для Spring, так и для Guice во всех наших примерах. Так что для нас это не новая концепция. Но давайте просто перечислим его еще раз для полноты картины.

В случае внедрения зависимостей на основе полей мы внедряем зависимости, помечая их с помощью @Autowired или @Inject .

4.6. Внедрение полей в Guice

Как мы упоминали в разделе выше, мы уже рассмотрели внедрение полей для Guice с помощью @Inject .

5. Вывод

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