Spring 学习笔记之 Bean 的作用域

在默认情况下,Spring 的应用上下文中所有的 bean 都是单例的形式创建的。也就是说,不管给定的一个 bean 被注入到其它 bean 多少次,每次注入的都是同一个实例。

在大多数情况下,单例 bean 是非常理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理。

有时候你所使用的类可能是易变的,它们会保持一些状态,比如我们在 Web 购物商城中常见的购物车功能,不同的用户不可能同时使用同一个购物车实例,因此重用是不安全的。

(一)Spring 中的作用域

Spring 提供了多种作用域,可以基于这些作用域来创建 bean,包括:

  • 单例(Singleton):在整个应用中,只创建 bean 的一个实例;
  • 原型(Prototype):每次注入或者通过 Spring 上下文获取的时候,都会创建一个新的 bean 实例;
  • 会话(Session):在 Web 应用中,为每个会话创建一个 bean 实例;
  • 请求(Request):在 Web 应用中,为每次请求创建一个 bean 实例;

如果需要自定义 bean 的作用域,需要使用 @Scope 注解,他可以与 @Component 或 @Bean 组合使用:

1
2
3
4
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Cake implements Dessert {
}

这里使用 ConfigurableBeanFactory 类的 SCOPE_PROTOTYPE 常亮设置为原型作用域。当然你也可以使用下面这种方式:

1
2
3
4
@Component
@Scope("prototype")
public class Cake implements Dessert {
}

但是尽可能使用 ConfigurableBeanFactory.SCOPE_PROTOTYPE,这更不容易出错。

当然也可以在 Java 配置中将作用域设置为原型 bean,例如:

1
2
3
4
5
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Dessert cake(){
return new Cake();
}

同样,也可以在 XML 中配置,应用元素的 scope 属性:

1
<bean id="cake" class="cn.javacodes.spring.beans.Cake" scope="prototype"></bean>

(二)使用会话和请求作用域

在 Web 应用中,我们经常需要操作两种作用域:会话和请求。

就像前面所说,在购物商城的购物车实例上,单例和原型作用域自然不能满足我们的需求,我们希望为每一个会话都创建一个购物车,那么这里会话作用域就是最完美的选择。

下面来简单模拟一下购物车的作用域场景:

1
2
3
4
5
6
7
@Bean
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){
// ....
}

这里,将 value 值设置成了 WebApplicationContext 中的 SCOPTE_SESSION 常量(值为 session)。这会告诉 Spring 为 Web 应用中的每个会话创建一个 ShoppingCart。对于每一个会话来说,这个 bean 实际上相当于是单例的。

这里需要注意,@Scope 还有一个 proxyMode 属性,它被设置为 ScopedProxyMode.INTERFACES。我们先不考虑这个属性,先来理解一下对 Spring 作用域的理解。

现在假设我们要将 ShoppingCart bean 注入到单例 StoreService bean 的 Setter 中,如下所示:

1
2
3
4
5
6
7
8
9
10
@Component
public class StoreService {

private ShoppingCart shoppingCart ;

@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart = shoppingCart;
}
}

因为 StoreService 是一个单例 bean,会在 Spring 上下文加载的时候创建,当它创建的时候,Spring 会试图将 ShoppingCart bean 注入到 setShoppingCart () 方法中。但是 ShoppingCart bean 是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话以后,才会出现 ShoppingCart 实例。

另外,系统中将会存在多个 ShoppingCart 实例:每个用户一个。我们并不想让 Spring 注入到某个胡定的 ShoppingCart 实例到 StoreService 中。我们希望的是当 StoreService 处理购物车功能时,它所使用的 ShoppingCart 实例恰好是当前会话所对应的那一个。

Spring 并不会将实际的 ShoppingCart bean 注入到 StoreService 中,Spring 会注入一个到 ShoppingCart bean 的代理。这个代理会暴露与 ShoppingCart 相同的方法,所以 StoreService 会认为他就是一个购物车。但是,当 StoreService 调用 ShoppingCart 的方法时,代理会对其进行懒解析并将调用委托给作用域内真正的 ShoppingCart bean。如下图所示:

现在我们来讨论一下 proxyMode 属性。我们将 proxyMode 属性设置为了 ScopedProxyMode.INTERFACES,这表明这个代理要实现 ShoppingCart 接口,并将调用委托给实现 bean。

这里我们的 ShoppingCart 是接口而不是具体的类,这当然是可以的(也是最理想的代理模式)。但如果 ShoppingCart 是具体的实现类而不是接口的话,Spring 就没办法创建基于接口的代理了。此时必须使用 CGLib 来生成基于类的代理。所以,如果 bean 类型是具体的类的话,我们必须要将 proxyMode 设置为 ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

请求作用域与会话作用域十分类似,也应该以作用域代理的方式进行注入,再次不做赘述。

(三)在 XML 中声明作用域代理

在 XML 中设置作用域代理需要使用 Spring aop 命名空间的一个元素:

1
2
3
<bean id="cart" class="cn.javacodes.spring.beans.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>

当然了,在使用 aop 命名空间之前一定要在 xml 的顶部进行对命名空间进行声明:

1
2
3
4
5
6
7
8
9
10
11
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
......
</beans>

注意:在使用 Spring 开发 web 项目时,需要在 web.xml 中加入如下内容(web2.4 以上):

1
2
3
4
5
6
7
<web-app>
...
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
...
</web-app>

web 2.4 以下版本需要加入:

1
2
3
4
5
6
7
8
9
10
11
12
<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>

另外,<aop:scoped-proxy /> 是与 @Scope 注解的 proxyMode 属性功能相同的 Spring XML 配置元素。它会告诉 Spring 为 bean 创建一个作用域代理。默认情况下,它会使用 CGLib 创建目标类的代理。但是我们也可以将 proxy-target-class 属性设置为 false,进而要求它生成基于接口的代理:

1
2
3
<bean id="cart" class="cn.javacodes.spring.beans.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>