bean 的作用域

2020/10/29 34

Spring

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

在大多数情况下,单例 bean 是很理想的方案,但有时候,你可能会发现,所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将 class 声明为单例的 bean 就不是什么好主意了,因为对象会被污染,使用时会出现意想不到的问题。

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

单例是默认的作用域,对于易变类型,这并不合适。如果选择其他的作用域,要使用 @Scope 注解,它可以与 @Component 或 @Bean 一起使用。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad { ... }

如果你使用 XML 来配置 bean 的话,可是使用 元素的 scope 属性来设置作用域。

使用 Session 和 Request 作用域

在 Web 应用中,如果能够实例化在 Session 和 Request 范围内的共享 bean,那将是非常有价值的事情。例如,在典型的电商应用中,可能会有一个 bean 代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都操作同一个购物车。另一方面,如果购物车是原型作用域的话,那么在应用中某一个地方添加了商品,在另一个地方发现购物车仍然是空状态。

就购物车 bean 来说,Session 作用域是最为合适的,因为它与给定的用户关联性最大。要指定 Session 作用域,我们可以使用 @Scope 注解。

@Component
@Scope(
    value = WebApplicationContext.SCOPE_SESSION,
    proxyMode = ScopedProxyMode.INTERFACES
)
public ShoppingCart cart() { ... }

要注意的是,@Scope 同时还有一个 proxyMode 属性,它被设置成 ScopedProxyMode.INTERFACES。这个属性解决了将 Session 或 Request 作用域的 bean 注入到单例 bean 中所遇到的问题。在描述 proxyMode 属性之前,我们先来看一下 proxyMode 所解决问题的场景。

假设我们要将 ShoppingCart bean 注入到单例 StoreService bean 的 Setter 方法中:

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

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

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

作用域代理能够延迟注入 Request 和 Session 作用域的 bean

图 1 作用域代理能够延迟注入 Request 和 Session 作用域的 bean

如配置所示,proxyMode 属性被设置成了 ScopedProxyMode.INTERFACES,这表明代理要实现 ShoppingCart 接口,并将调用委托给实现 bean。

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

如果你需要使用 XML 配置来声明 Session 或 Request 作用域的 bean:

<bean id="cart" class="wang.doghappy.ShoppingCart" scope="session">
    <aop:scoped-proxy />
</bean>

<aop:scoped-proxy /> 告诉 Spring 为 bean 创建一个作用域代理,默认会使用 CGLib 创建目标的代理,但是我们可以将 proxy-target-class 设置为 false,要求它生成基于接口的代理:

<bean id="cart" class="wang.doghappy.ShoppingCart" scope="session">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

为了使用 <aop:scoped-proxy /> 元组,需要在 XML 中声明命名空间:

xmlns:aop="http://www.springframework.org/schema/aop"