参考:Bean Scopes :: Spring Framework
在Spring框架中,Bean是指由Spring IoC容器实例化、组装和管理的对象。
后面的4个scope只在具有 Web 支持的 SpringApplicationContext中有效。
| Scope | Description |
|---|---|
| singleton | (默认)单个bean定义在每个Spring IoC容器中只有一个实例 |
| prototype | 单个bean定义可以有多个实例 |
| request | 将单个 bean 定义的作用域限定为单个 HTTP 请求的生命周期。每次HTTP请求都会根据bean定义创建自己的bean实例。 |
| session | 将单个 bean 定义的作用域限定为HTTPsession的生命周期 |
| application | 将单个 bean 定义的作用域限定为ServletContext的生命周期 |
| websocket | 将单个 bean 定义的作用域限定为WebSocket的生命周期 |
Spring提供了线程作用域,但默认情况下未注册。更多信息请查阅
SimpleThreadScope
1. singleton作用域
当你定义一个singleton作用域的bean时,Spring IoC容器只会根据该定义创建一个实例,可称作单例。单例会被存储在缓存中,后续的请求和引用将直接从缓存中获取。
Spring中的单例是指每个IoC容器中的每个bean只有一个实例。singleton作用域是Spring中默认的作用域,下面的代码中,两个定义是等价的:
@Bean
public DefaultAccountService defaultAccountService() {
return new DefaultAccountService();
}
@Bean
@Scope("singleton")
public DefaultAccountService defaultAccountService2() {
return new DefaultAccountService();
}
2. prototype作用域
拥有prototype作用域的bean会在每次被需要时创建一个新的实例。一般来说,有状态的bean使用prototype,无状态的bean使用singleton(状态可以指某个对象的属性和数据成员的集合)。
作用域为prototype意味着你可以使用一个bean创建多个实例,每个实例中保存不同的数据,它们之间不会互相影响,比如每个实例对应一个用户的数据。
@Bean
@Scope("prototype")
public DefaultAccountService defaultAccountService() {
return new DefaultAccountService();
}
Spring并不会管理prototype bean的完整生命周期。容器会初始化、配置,或者以其他方式组装prototype对象,将其交给客户端,而不会记录创建的实例。初始化阶段的生命周期回调方法会被调用;但使用prototype时,生命周期中销毁时的回调方法不会被调用。所以客户端代码必须对此类对象进行资源释放。 要想让Spring容器释放prototype bean的资源,请配置 bean post-processor,它会引用需要被清理的bean。
从某种意义上讲,Spring容器对于prototype bean的作用是替代Java中的new关键字。在初始化之后,这个bean实例的所有生命周期都需要由客户端来处理。(关于Spring容器中Bean生命周期的详细信息,请查阅Lifecycle Callbacks)
3. 单例中注入prototype bean
singleton bean中注入prototype bean只会发生一次,即在singleton bean初始化的时候,所以singleton bean只会拥有prototype bean的一个实例。
如果你想在运行时使用prototype bean的多个不同实例,需要用到方法注入。
4. Web环境下的作用域
只有当你使用了支持web的SpringApplicationContext实现(比如XmlWebApplicationContext),才能使用request,session,application和websocket作用域。如果使用常规的Spring IoC容器,比如ClassPathXmlApplicationContext,程序会因为你了使用未知作用域而抛出IllegalStateException。
4.1 初始化Web配置
为了使用request,session,application和websocket作用域,你需要在定义bean之前进行一些初始化配置。如何进行初始化配置取决于你的Servlet环境。
如果使用了Spring Web MVC,你并不需要进行任何配置。SpringMVC中的请求将会由SpringDispatcherServlet处理,而DispatcherServlet已经进行了合适的配置。
如果你使用的Servlet web容器中的请求不是由DispatcherServlet处理的(比如JSF),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。你可以通过WebApplicationListener接口来实现,也可以在web.xml中进行配置:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果listener的配置存在问题,也可以使用SpringRequestContextFilter。请根据需求对filter映射进行合适的修改:
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
DispatcherServlet, RequestContextListener和RequestContextFilter的作用是一样的,即将 HTTP 请求对象绑定到为该请求提供服务的Thread。
4.2 Request作用域
假设XML配置中有如下bean定义:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring容器会为每一个HTTP请求创建一个新的LoginAction实例,当请求处理完之后,bean也会被丢弃。
可以使用@RequstScope来指定作用域为request:
@RequestScope
@Component
public class LoginAction {
// ...
}
以下是@RequestScope的定义:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("request")
public @interface RequestScope {
@AliasFor(
annotation = Scope.class
)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
4.3 Session作用域
假设XML配置中有如下bean定义:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring容器会为每一个HTTPSession创建一个UserPreferences实例,当HTTPSession结束的时候,bean也会被丢弃。
可以使用@SessionScope来指定作用域为session:
@SessionScope
@Component
public class UserPreferences {
// ...
}
HTTP request和session的关系:每次请求是无状态的,session可以用于在多个请求之间保存状态。session是基于cookie实现的,session由服务端创建并存储,客户端会记录session的索引信息(如Session ID),之后的请求将携带cookie(比如SESSION=ce0f67ee-5240-4e02-a240-b17ef35b92c8)访问服务器,那么携带这个cookie的请求就可以看作是一个会话请求。
4.4 Application作用域
假设XML配置中有如下bean定义:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring容器会为整个Web应用创建一个新的AppPreferences,当然,只会创建一次。appPreferences的作用域是ServletContext,它将成ServletContext的一个属性。这和singleton类似,但有两点不同:它在每个ServletContext中有一个实例,而不是每个SpringApplicationContext,后者在一个web应用中可能存在多个;它是公开的,是ServletContext中的一个属性。
可以使用@ApplicationScope来指定作用域为application:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
4.5 WebSocket作用域
Websocket作用域和Websocket session的生命周期相关联,适用于STOMP over WebSocket应用程序。
作用域为websocket的bean可以被注入到controller中,或者任意注册在clientInboundChannel的channel拦截器中。这个bean通常是一个单例,会比任何Websocket session都要活的久。所以需要使用作用域代理。
假设有两个bean,controller和userInfo,它们的作用域分别为singleton和websocket(使用了代理)。现在将userInfo注入到controller中,实际上,注入到controller中的是userInfo的代理对象,只会在controller初始化的时候被创建并注入一次。
之后,controller处理session请求的时候,当使用到userInfo中的方法时,Spring会通过CGLIB动态创建新的userInfo实例,通过代理去访问这个实例。所以,每个session都对应一个不同的userInfo实例,这个实例随着session的创建而创建,session结束之后也会随之销毁。
为了验证,我进行了简单的测试。
下面的案例可能不太合理(service层中的bean一般都是单例),这里只是为了测试。
现有以下配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello-websocket");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic", "/queue");
}
}
每当有客户端通过session连接向/topic/hello发送消息时,请求将交由 GreetingController中的greeting()方法进行处理:
@Controller
public class GreetingController {
private CommonService commonService;
@Autowired
void setCommonService(CommonService commonService) {
this.commonService = commonService;
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
commonService.sayHello();
return new Greeting("Hello, " + message.getName() + "!");
}
}
将CommonService的作用域设置为websocket,sayHello()方法用于输出当前session中的实例信息(每个session对应一个实例):
@Slf4j
@Service
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CommonService {
public void sayHello() {
log.info("I am " + this.getClass().getName() + "@" + Integer.toHexString(hashCode()));
}
}
如果不启用代理,运行时会报错:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'greetingController': Unsatisfied dependency expressed through method 'setCommonService' parameter 0: Error creating bean with name 'commonService': Scope 'websocket' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton
我创建了3个session,其中红色框中的信息是由同一个session输出的。这意味着,不同session中的bean实例是不一样的,同一个session中使用的是同一个实例。
接着,我修改了controller,在greeting()方法中输出commonService的类信息:
@Slf4j
@Controller
public class GreetingController {
private CommonService commonService;
@Autowired
void setCommonService(CommonService commonService) {
this.commonService = commonService;
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000);
log.info(commonService.getClass().getName() + "@" + Integer.toHexString(commonService.hashCode()));
return new Greeting("Hello, " + message.getName() + "!");
}
}
commonService只会在GreetingController初始化的时候被注入一次,而被注入的commonService实际上是一个代理对象。针对于每一个websocket session连接,都会创建一个新的CommonService实例,代理对象会动态获取当前session的CommonService实例。
我从多个用户session中访问/topic/greetings,输出的结果都一样,这说明注入到controller中的commonService是一个代理对象,而且是一个单例:
4.6 将Scoped Bean作为依赖
当你将一个作用域较短的bean注入到一个作用域较长的bean时,你可能需要使用AOP代理来替换掉scoped bean。
当你在作用域为
prototype的bean中声明<aop:scoped-proxy>时,会导致每次调用代理对象方法的时候都创建一个新的实例。你可以将你的注入点声明为
ObjectFactory<MyTargetBean>,每当需要当前实例时,通过getObject()方法获取。这样就不需要单独存储实例。你也可以声明
ObjectProvider<MyTargetBean>,它提供了几个额外的方法,包括getIfAvailable和getIfUnique。JSR-330中提供了
Provider类,对应的声明为Provider<MyTargetBean>,通过调用get()方法可以获取当前实例。
接下来,我将通过一个具体的使用场景,来介绍为什么需要使用代理。
假设有以下配置,userService中需要注入userPreferences(以bean的id代指bean),userPreferences的作用域为session,userService的作用域为singleton。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
因为作用域为singleton,userService只会发生一次依赖注入,即它初始化的时候,所以只会有一个userPreferences实例注入到userService中。
现在,假设有多个session,每个session对应一个用户的请求。userService在处理第一个session的时候,userPreferences中将会保存当前用户的数据,那么当它处理第二个session时,就可能出现以下两种情况:1. userPreferences中记录的是上一个session的状态;2. userPreferences随着上一个session的结束被销毁了(受到作用域的限制),这显然不合理。
为了解决这个问题,我们采用动态代理的方式。真正的UserPreferences实例并不会被注入到userService中,取而代之的是它的动态代理。这个代理是一个单例,只会被注入一次,当userService调用它的方法时,它就会从当前的session中获取到真正的UserPreferences实例,并调用这个实例对应的方法,而userService是无法察觉到代理的。
选择代理类型
当Spring容器为<aop:scoped-proxy>标记的bean创建代理时,会创建基于CGLIB的类代理。
CGLIB代理只会拦截public方法的调用,非public方法不会代理到真正的实例。
你可以通过将<aop:scoped-proxy/>上的proxy-target-class属性设置为false,让Spring容器创建标准JDK中基于接口的代理。使用基于接口的代理意味着你无需在类路径中添加额外的依赖(jdk提供了支持)。但是,拥有作用域的bean在类定义时必须实现至少一个接口,需要注入这个bean的其他bean必须通过它的一个接口来引用它。
DefaultUserPreferences实现了UserPreferences接口。如果某个bean需要注入DefaultUserPreferences实例,需要通过它实现的接口UserPreferences来引用它:
public interface UserPreferences {
String getPreference(String username);
}
public class DefaultUserPreferences implements UserPreferences{
@Override
public String getPreference(String username) {
return "default preference";
}
}
UserManager中需要注入DefaultUserPreferences实例。userPreferences字段的类型只能是接口类型,比如UserPreferences,如果改为实现类的类型,会报错。因为代理对象会实现这个接口然后注入到UserManager中,如果是实现类的类型,会导致类型不匹配。
public class UserManager {
private UserPreferences userPreferences;
public void setUserPreferences(UserPreferences userPreferences) {
this.userPreferences = userPreferences;
}
public UserPreferences getUserPreferences() {
return userPreferences;
}
}
通过XML进行配置,userPreferences的作用域为session:
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.weedien.ioccfg.scope.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.weedien.ioccfg.scope.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在这个案例中,userManager中注入的bean实际上是一个代理对象,这个代理对象为jdk.proxy2.$Proxy60@668456d8,它实现了UserPreferences接口。代理对象唯一且只会被注入一次,通过代理对象访问真正的UserPreferences实例,每个session都有一个UserPreferences实例。
5. 自定义作用域
你可以定义自己的作用域,甚至可以重新定义已有的作用域(singleton和prototype除外)。
5.1 创建自定义作用域
自定义的作用域需要实现org.springframework.beans.factory.config.Scope接口。
Scope接口有4个方法,可以从作用域中获取对象、移除对象和销毁对象。
从底层作用域中获取对象:
Object get(String name, ObjectFactory<?> objectFactory)
从底层作用域中移除对象:
Object remove(String name)
注册一个回调方法,当作用域结束或指定对象销毁时调用:
void registerDestructionCallback(String name, Runnable destructionCallback)
从底层作用域中获取对话标识符:
String getConversationId()
每个作用域的标识符都是不一样的,比如在session作用域的实现中,getConversationId()方法将返回session标识符。
5.2 使用自定义作用域
下面将以SimpleThreadScope为例,介绍如何使用自定义scope。SimpleThreadScope定义在org.springframework.context.support中,但默认情况下不启用。
在使用自定义的scope之前,需要在Spring容器中进行注册,注册需要用到以下方法:
void registerScope(String scopeName, Scope scope);
该方法定义在ConfigurableBeanFactory接口中,Spring附带的大部分ApplicationContext实现中都包含这个接口。
下面是一个可行的配置,用来注册自定义scope:
@Bean
public BeanFactory registerScope(ConfigurableBeanFactory beanFactory) {
org.springframework.beans.factory.config.Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
return beanFactory;
}
也可以通过CustomScopeConfigurer来注册,这与上面的配置是等效的:
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
customScopeConfigurer.addScope("thread", new SimpleThreadScope());
return customScopeConfigurer;
}
等价的XML配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
</beans>
接下来,你可以使用自定义的scope来定义bean:
@Bean
@Scope("thread")
public MyValueCalculator myValueCalculator() {
return new MyValueCalculator();
}