在默认情况下,Spring的应用上下文中所有的bean都是单例的形式创建的。也就是说,不管给定的一个bean被注入到其它bean多少次,每次注入的都是同一个实例。
在大多数情况下,单例bean是非常理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理。
有时候你所使用的类可能是易变的,它们会保持一些状态,比如我们在Web购物商城中常见的购物车功能,不同的用户不可能同时使用同一个购物车实例,因此重用是不安全的。
(一)Spring中的作用域
Spring提供了多种作用域,可以基于这些作用域来创建bean,包括:
-
单例(Singleton):在整个应用中,只创建bean的一个实例;
-
原型(Prototype):每次注入或者通过Spring上下文获取的时候,都会创建一个新的bean实例;
-
会话(Session):在Web应用中,为每个会话创建一个bean实例;
-
请求(Request):在Web应用中,为每次请求创建一个bean实例;
如果需要自定义bean的作用域,需要使用@Scope注解,他可以与@Component或@Bean组合使用:
@Component @Scope(ConfigurableBeanFactory.SCOPEPROTOTYPE) public class Cake implements Dessert { }
这里使用ConfigurableBeanFactory类的SCOPEPROTOTYPE常亮设置为原型作用域。当然你也可以使用下面这种方式:
@Component @Scope("prototype") public class Cake implements Dessert { }
但是尽可能使用 ConfigurableBeanFactory . SCOPEPROTOTYPE, 这更不容易出错。
当然也可以在Java配置中将作用域设置为原型bean,例如:
@Bean @Scope(ConfigurableBeanFactory.SCOPEPROTOTYPE) public Dessert cake(){ return new Cake(); }
同样,也可以在XML中配置,应用<bean>元素的scope属性:
<bean id="cake" class="cn.javacodes.spring.beans.Cake" scope="prototype"></bean>
(二)使用会话和请求作用域
在Web应用中,我们经常需要操作两种作用域:会话和请求。
就像前面所说,在购物商城的购物车实例上,单例和原型作用域自然不能满足我们的需求,我们希望为每一个会话都创建一个购物车,那么这里会话作用域就是最完美的选择。
下面来简单模拟一下购物车的作用域场景:
@Bean @Scope(value = WebApplicationContext.SCOPESESSION,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中,如下所示:
@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命名空间的一个元素:
<bean id="cart" class="cn.javacodes.spring.beans.ShoppingCart" scope="session"> <aop:scoped-proxy /> </bean>
当然了,在使用aop命名空间之前一定要在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" 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以上):
<web-app> ... <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> ... </web-app>
web 2.4以下版本需要加入:
<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,进而要求它生成基于接口的代理:
<bean id="cart" class="cn.javacodes.spring.beans.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean>