Spring beans scopes указывают Sping, как ему управлять жизненным циклом бина. Из коробки Spring поддерживает два базовых scope: Singleton и Prototype. По умолчанию каждый бин имеет scope Singleton, если не указано обратное.
Отличие этих двух scope в том, что Singleton bean создаётся в единственном экземпляре на весь контекст и ссылка на этот экземпляр подсовывается каждому другому бину, который просит эту зависимость. А вот Prototype bean напротив, создаётся отдельный для каждого другого бина, который просит его в зависимость.
Лучше всего это продемонстрировать в коде:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/**
* Simple stateful bean.
*/
public interface StatefulBean {
/**
* Gets bean state.
* @return state value.
*/
String getState();
/**
* Sets bean state.
* @param state new state value.
*/
void setState(String state);
}
|
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
|
/**
* Example of default scope bean (Singleton)
*/
@Service
public class SingletonBean implements StatefulBean {
/**
* Some state.
*/
private String state;
@PostConstruct
public void init() {
this.state = "Initial state";
}
@Override
public final String getState() {
return state;
}
@Override
public final void setState(final String s) {
this.state = s;
}
}
|
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
|
/**
* Example of custom scope bean (Prototype)
*/
@Service
@Scope(value = "prototype")
public class PrototypeBean implements StatefulBean {
/**
* Some state.
*/
private String state;
@PostConstruct
public void init() {
this.state = "Initial state";
}
@Override
public final String getState() {
return state;
}
@Override
public final void setState(final String s) {
this.state = s;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* Bean consumer for differently scoped beans.
*/
@Service
public class FirstConsumer {
private final static Logger log = Logger.getLogger(FirstConsumer.class.getName());
@Inject
private StatefulBean singletonBean;
@Inject
private StatefulBean prototypeBean;
public void processState() {
log.info("singletonBean state is: " + singletonBean.getState());
log.info("prototypeBean state is: " + prototypeBean.getState());
singletonBean.setState("After FirstConsumer");
prototypeBean.setState("After FirstConsumer");
log.info("singletonBean state set to: " + singletonBean.getState());
log.info("prototypeBean state set to: " + prototypeBean.getState());
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* Bean consumer for differently scoped beans.
*/
@Service
public class SecondConsumer {
private final static Logger log = Logger.getLogger(SecondConsumer.class.getName());
@Inject
private StatefulBean singletonBean;
@Inject
private StatefulBean prototypeBean;
public void processState() {
log.info("singletonBean state is: " + singletonBean.getState());
log.info("prototypeBean state is: " + prototypeBean.getState());
singletonBean.setState("After SecondConsumer");
prototypeBean.setState("After SecondConsumer");
log.info("singletonBean state set to: " + singletonBean.getState());
log.info("prototypeBean state set to: " + prototypeBean.getState());
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
public static void main(final String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext("ru.easyjava.spring");
FirstConsumer firstConsumer = context.getBean(FirstConsumer.class);
SecondConsumer secondConsumer = context.getBean(SecondConsumer.class);
firstConsumer.processState();
secondConsumer.processState();
firstConsumer.processState();
}
|
Кода много 🙂 У нас есть два бина, каждый из которых имеет какое-то хранимое состояние. На один из бинов навешена аннотация @Scope("prototype"), которая объявляет спрингу, что этот бин имеет scope Prototype, очевидно.
От бинов с разным scope зависят два бина потребителя, каждый из которых меняет внутреннее состояние. Какой результат будет после запуска?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
FirstConsumer processState
INFO: singletonBean state is: Initial state
FirstConsumer processState
INFO: prototypeBean state is: Initial state
FirstConsumer processState
INFO: singletonBean state set to: After FirstConsumer
FirstConsumer processState
INFO: prototypeBean state set to: After FirstConsumer
SecondConsumer processState
INFO: singletonBean state is: After FirstConsumer
SecondConsumer processState
INFO: prototypeBean state is: Initial state
SecondConsumer processState
INFO: singletonBean state set to: After SecondConsumer
SecondConsumer processState
INFO: prototypeBean state set to: After SecondConsumer
FirstConsumer processState
INFO: singletonBean state is: After SecondConsumer
FirstConsumer processState
INFO: prototypeBean state is: After FirstConsumer
FirstConsumer processState
INFO: singletonBean state set to: After FirstConsumer
FirstConsumer processState
INFO: prototypeBean state set to: After FirstConsumer
|
Видно, что внутреннее состояние SingletonBean общее и для FirstConsumer и для SecondConsumer, а вот внутреннее состояние PrototypeBean и первый и второй бины-потребители видят разные. То есть подтверждается, что SingletonBean создан в единственном экземпляре, а PrototypeBean в двух экземплярах.
В этом месте многие руководства начинают рекомендовать использовать бины с scope singleton для stateless бинов, а бины со scope prototype для stateful бинов. С одной стороны, они конечно правы,так как Spring гарантирует, что данные scope prototype бинов не будут разделены между разными бинами, вызывая конфликты при обращении. С другой стороны, scope prototype не гарантирует потокобезопасности и не гарантирует, что один и тот же экземпляр бина со scope prototype будет вызван не более чем в одном потоке, со всеми вытекающими последствями.
Задание scope
Как показано выше, аннотация @Scope задаёт scope бину. Эта же аннотация, только приложенная к методу, задаёт scope бину при использовании java config:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* Spring context configuration descriptor.
*/
public class ContextConfiguration {
/**
* Prototype bean
* @return Prototype bean instance
*/
@Bean
@Scope("prototype")
public StatefulBean prototypeBean() {
return new PrototypeBean();
}
}
|
С xml тоже всё просто: в описание бина добавляется ещё один параметр:
1
|
<bean id="prototypeBean" scope="prototype" class="ru.easyjava.spring.SingletonBean"/>
|
Аналогично и с groovy configuration:
1
2
3
4
5
|
beans {
prototypeBean(PrototypeBean){bean->
bean.scope = 'prototype'
}
}
|
Дополнительные scopes
Кроме двух базовых scopes, о которых речь шла выше, Spring предоставляет ещё три scope, которые доступны только при использовании web специфичных ApplicationContext:
- request — бин создаётся для каждого HTTP request и уничтожается после завершения обработки.
- session — бин создаётся для HTTP сессии и уничтожается, после её завершения.
- globalSession — редкий зверь, используемый с портлетами. Бин создаётся на время существования глобальной сессии всего приложения с портлетами.
Внедрение бинов и иерархия scope
У человека, внимательно прочитавшего про web специфичные scopes может возникнуть вопрос — а что если я возьму и сделаю какой-нибудь singleton bean зависящим от request bean? Первый существует всегда и он один, второй существует от случая к случаю, зато может существовать в нескольких экземплярах. Ответ на этот вопрос простой — ошибка будет 🙂 Бины с разными scope не совсем между собой совместимы.
Singleton bean может быть зависимостью для бина с любым scope. Сам он может зависеть от бина с singleton scope и prototype scope, причём для каждого singleton бина будет создан свой собственный уникальный экземпляр prototype бина. Кстати, получить в singleton бине новый экземпляр prototype бина так просто не получится, надо вручную обращаться к ApplicationContext и просить создать новый бин.
Дальнейшая иерархия сохраняется: prototype bean может быть зависимостью для бина с любым scope. Сам он может зависеть от бина с singleton scope и prototype scope.
globalSession бин может быть зависимостью для бинов с scope globalSession, session и request. Сам он может зависеть от бина с singleton scope, prototype scope и globalSession scope.
session бин, очевидно, может быть зависимостью для бинов с session и request. Сам он может зависеть от бина с singleton scope, prototype scope, globalSession и session scope.
Наконец request бин может быть зависимостью только для других request бинов, зато сам он может зависет от бина с любым scope.
Scoped proxies
На самом деле в разделе выше написана неправда. И request и session и globalSession могут быть зависимостями для любых бинов, но не напрямую. Мы можем попросить spring создать специальный прокси, который с одной стороны будет иметь (внутри себя) scope singleton и, таким образом, быть совместимым со всеми scopes, с другой стороны этот прокси будет определять требуемый в данном контексте исполнения бин с меньшим временем жизни (например session бин) и передавать все вызовы ему.
Попросить spring создать прокси можно разными путями. Во-первых в аннотацию @Scope можно передать proxyMode=INTERFACES или proxyMode=TARGET_CLASS, который создадут для класса proxy, основанный на интерфейсах, которые реализует класс или сгенерирует специальный отдельный класс, во втором случае.
Для тех, кто предпочитает xml конфигурацию, можно включить генерацию прокси для конкретного класса:
1
2
3
|
<bean id="requestBean" scope="request" class="org.requestBean">
<aop:scoped-proxy/>
</bean>
|
Либо для всех классов:
1
|
<context:component-scan base-package="ru.easyjava.spring" scoped-proxy="interfaces" />
|
Код примера доступен на github.