Spring в первую очередь известен как IoC контейнер, реализующий шаблон проектирования «Внедрение зависимостей».
Не вдаваясь в подробности, внедрение зависимостей можно описать как связывание компонентов приложения во время исполнения. Другими словами, если бину ServiceA нужен для работы бин, имеющий тип RepositoryB, Spring сконструирует этот бин и внедрит его в бин ServiceA.
Подготовка
Поскольку Spring поддерживает несколько вариантов конфигурации контекста, создадим многомодульный maven проект, по одному модулю на каждый вариант. Из зависимостей понадобятся сам Spring, JavaEE из-за JSR-299 и в этот раз можно обойтись без тестов.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javaee.version>7.0</javaee.version>
<org.springframework.version>4.1.7.RELEASE</org.springframework.version>
<junit.version>4.12</junit.version>
<hamcrest.version>1.3</hamcrest.version>
</properties>
<modules>
<module>xml</module>
<module>annotations</module>
<module>javaconfig</module>
<module>groovy</module>
</modules>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
|
Внедрение с помощью конструктора
Наиболее очевидный вариант внедрения, когда зависимости передаются в конструктор класса:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class BaseRepository {
int count() { return 10; }
}
public class BaseService {
Boolean isMany(Integer number) { return number > 5;}
}
public class DependentService {
private final BaseRepository repository;
private final BaseService service;
public DependentService(final BaseRepository r, BaseService s) {
this.repository = r;
this.service = s;
}
public String process() {
if (service.isMany(repository.count())) {
return "Too big number";
} else
return "Everything is fine";
}
}
|
Ничего Spring специфичного тут пока нет. Использование этих классов в Spring отличается для каждого метода инициализации.
Для JavaConfig всё выглядит как обыкновенный java код:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class ContextConfiguration {
@Bean
public BaseRepository baseRepository() { return new BaseRepository(); }
@Bean
public BaseService baseService() { return new BaseService(); }
@Bean
public DependentService dependent() { return new DependentService(baseRepository(), baseService()); }
}
public final class App {
/**
* Application entry point.
* @param args Array of command line arguments.
*/
public static void main(final String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(ContextConfiguration.class);
DependentService dependent = context.getBean(DependentService.class);
System.out.println(dependent.process());
}
}
|
Надо понимать, что методы с аннотацией @Bean перерабатываются Spring’ом и возвращают полностью инициализированные бины, а не просто «сырые» объекты. Например, если использовать baseService() для инициализации более чем одного бина, они все получат один и тот же экземпляр класса BaseService.
Для XML конфигурации код будет немного другой:
1
2
3
4
5
6
7
8
|
<bean id="baseRepository" class="ru.easyjava.spring.BaseRepository"/>
<bean id="dependent" class="ru.easyjava.spring.DependentService">
<constructor-arg index="0" ref="baseRepository"/>
<constructor-arg name="s">
<bean class="ru.easyjava.spring.BaseService"/>
</constructor-arg>
</bean>
|
В XML можно передавать бины в конструктор как позиционно, так и по имени параметра. Кроме того, можно передавать и по типу параметра:
1
|
<constructor-arg type="ru.easyjava.spring.BaseRepository" ref="BaseRepository"/>
|
Бины можно как создавать заранее и ссылаться на них позднее (разумеется, более чем один раз), так и создавать сразу по месту. А можно передавать не бины, а какие либо значения.
Конфигурация groovy отличается не сильно:
1
2
3
4
5
6
|
beans {
base BaseService
repository BaseRepository
dependent(DependentService, repository, base)
}
|
Однако создать бин по месту тут нельзя и передавать параметры в конструктор можно только позиционно.
Использование аннотаций требует изменения кода сервисов:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Repository
public class BaseRepository {
int count() { return 10; }
}
@Service
public class BaseService {
Boolean isMany(Integer number) { return number > 5;}
}
@Service
public class DependentService {
private final BaseRepository repository;
private final BaseService service;
@Inject
public DependentService(final BaseRepository r, BaseService s) {
this.repository = r;
this.service = s;
}
public String process() {
if (service.isMany(repository.count())) {
return "Too big number";
} else
return "Everything is fine";
}
}
|
Аннотации @Service и @Repository специфичны для Spring и описывают разные типы Spring бинов. Аннотация @Inject является стандартной аннотацией JSR-299 и указывает, что параметры конструктора должны быть внедрены при его создании. Внедрение, в данном случае, производится по типам.
Внедрение с помощью сеттеров
Практически тоже самое, что и внедрение с конструкторами, только вид с другой стороны: объект вначале конструируется, а потом внедряются зависимости с помощью сеттеров.
Начнём с java config как с самого простого:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class DependentService {
private BaseRepository repository;
private BaseService service;
public void setRepository(BaseRepository r) {
this.repository = r;
}
public void setService(BaseService s) {
this.service = s;
}
public String process() {
if (service.isMany(repository.count())) {
return "Too big number";
} else
return "Everything is fine";
}
}
|
Класс конфигурации теперь явно задаёт значения полей:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class ContextConfiguration {
@Bean
public BaseRepository baseRepository() { return new BaseRepository(); }
@Bean
public BaseService baseService() { return new BaseService(); }
@Bean
public DependentService dependent() {
DependentService dependent = new DependentService();
dependent.setRepository(baseRepository());
dependent.setService(baseService());
return dependent;
}
}
|
Для XML дескриптор тоже меняется:
1
2
3
4
5
6
7
8
|
<bean id="baseRepository" class="ru.easyjava.spring.BaseRepository"/>
<bean id="dependent" class="ru.easyjava.spring.properties.DependentService">
<property name="repository" ref="baseRepository"/>
<property name="service">
<bean class="ru.easyjava.spring.BaseService"/>
</property>
</bean>
|
К свойствам класса можно обращаться только по имени, то есть внедрение по типам не поддерживается.
В Groovy конфигурации к бинам добавляются имена полей, всё остальное без изменений:
1
2
3
4
5
6
7
8
9
10
|
import ru.easyjava.spring.BaseRepository
import ru.easyjava.spring.BaseService
import ru.easyjava.spring.properties.DependentService
beans {
base BaseService
repository BaseRepository
dependent(DependentService, repository:repository, service:base)
}
|
Отличие в инициализации с помощью аннотаций в том, что аннотации @Inject применяются теперь к сеттерам:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Service
public class DependentService {
private BaseRepository repository;
private BaseService service;
@Inject
public void setRepository(BaseRepository r) {
this.repository = r;
}
@Inject
public void setService(BaseService s) {
this.service = s;
}
public String process() {
if (service.isMany(repository. count())) {
return "Too big number";
} else
return "Everything is fine";
}
}
|
Непосредственное внедрение
Последние вариант внедрения зависимостей — внедрение непосредственно в поля объекта, используя reflection.
Непосредственное внедрение зависимостей требует использования аннотаций, поэтому я рассмотрю только пример с нициализацией с использованием аннотаций:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service
public class DependentService {
@Inject
private BaseRepository repository;
@Inject
private BaseService service;
public String process() {
if (service.isMany(repository. count())) {
return "Too big number";
} else
return "Everything is fine";
}
}
|
Всё! Аннотация @Inject применяется непосредственно к полям, а далее действует особая магия Spring.
Как же связывать?
Внедрение зависимостей напрямую выглядит наиболее удобным методом, требующим меньше всего затрат. Однако у этого метода есть недостаток: поскольку связывание производится через reflection, создание бина несколько замедляется. Если бин создаётся один раз при старте приложения, это не страшно. Но если он пересоздаётся, например, для каждого запроса, это может начать сказываться на общей производительности приложения.
Документация по Spring рекомендует использоваться конструкторы и сеттеры, если у бина есть обязательные и опциональные зависимости. Первые связываются через конструктор, вторые, соответственно, через сеттеры.
Впрочем аналогичный эффект можно получить и при прямом внедрении, аннотируя обязательные зависимости аннотацией @Required
Так же внедрение через конструкторы подвержено проблеме с замкнутыми зависимостями, когда бин А зависит от бина Б, который зависит от бина А. Если Spring столкнётся с такой зависимостью при внедрении через конструкторы, он выбросит исключение BeanCurrentlyInCreationException, а при использовании сеттеров бины будут успешно сконструированы.
Код примера доступен на github.