1.5 bean的作用域 #
当您创建一个bean定义时,您就意味着创建了一套定义方式用于创建由该bean定义所定义的类的实际实例。bean定义是一套定义方式的思想很重要,因为这意味着,与类一样,可以基于一套定义方式创建多个对象实例。
您不仅可以控制要插入到从特定bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定bean定义创建的对象的作用域。这种方法功能强大且灵活,因为您可以选择通过配置创建的对象的作用域,而不必在Java类级别烤入对象的作用域。可以将bean定义为部署在多个作用域中的一个。Spring框架支持六个作用域,其中四个只有在使用web相关的ApplicationContext
时才可用。您还可以创建自定义作用域。
下表描述了支持的作用域:
表3. Bean作用域
作用域 | 描述 |
---|---|
singleton | (默认)为每个Spring IoC容器将单个bean定义的作用域限定为单个对象实例。 |
prototype | 将单个bean定义的作用域限定为任意数量的对象实例。 |
request | 将单个bean定义的作用域限定为单次HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的bean实例,该实例是在单个bean定义的后面创建的。仅在支持web的Spring ApplicationContext 中有效。 |
session | 将单个bean定义的作用域限定为HTTP Session 的生命周期。仅在支持web的Spring ApplicationContext 中有效。 |
application | 将单个bean定义的作用域限定到ServletContext 的生命周期。仅在支持web的Spring ApplicationContext 中有效。 |
websocket | 将单个bean定义的作用域限定到WebSocket 的生命周期。仅在支持web的Spring ApplicationContext 中有效。 |
从Spring 3.0开始,线程作用域可以使用,但默认情况下是不注册的。有关更多信息,请参阅SimpleThreadScope
的文档。有关如何注册此作用域或任何其他自定义作用域的说明,请参阅使用自定义作用域。
1.5.1 单例作用域 #
一个单例bean被管理的只有一个共享实例,通过ID请求该bean定义所匹配bean的所有请求都会导致Spring容器返回该特定的bean实例。
换句话说,当您定义一个bean定义并将其作用域限定为一个单例时,Spring IoC容器只创建该bean定义所定义对象的一个实例。这个实例存储在这些单例bean的缓存中,后续所有对该命名bean的请求和引用都返回缓存对象。下图显示了单例作用域的工作原理:
Spring的单例bean概念不同于四人组(GoF)设计模式书中定义的单例模式。GoF单例硬编码对象的作用域,这样每个类加载器只创建一个特定类的实例。Spring单例的范围最好描述为每个容器和每个bean。这意味着,如果在单个Spring容器中为特定类定义一个bean,Spring容器将仅创建该bean定义的类的一个实例。单例作用域是Spring中的默认作用域。要在XML中将bean定义为单例,可以如下例所示定义bean:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 以下是等效的,尽管多余(默认是单例作用域) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2 原型作用域 #
bean部署为非单例原型作用域将导致每次对特定bean发出请求时都创建一个新的bean实例。也就是说,bean被注入到另一个bean中,或者通过容器上的getBean()
方法调用来请求它。通常,所有有状态bean都应该使用原型作用域,无状态bean应该使用单例作用域。
下图说明了Spring的原型作用域:
(数据访问对象(DAO)通常不配置为原型,因为典型的DAO不包含任何对话状态。我们更容易复用单例图的核心。)
以下示例在XML中将bean定义为原型:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域不同,Spring并不管理原型bean的整个生命周期。容器实例化、配置和以其他方式组装原型对象并将其交给客户端,而不再记录该原型实例。因此,尽管对所有对象调用初始化生命周期回调方法,而不考虑作用域,但对于原型,不会调用配置的销毁生命周期回调。客户端代码必须清理原型作用域的对象,并释放原型bean所持有的昂贵资源。要让Spring容器释放原型作用域的bean所持有的资源,请尝试使用定制的bean后置处理器,该后置处理器持有对需要清理的bean的引用。
在某些方面,Spring容器在原型作用域bean中的角色是Java new
操作符的替代品。所有超过该点的生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参阅生命周期回调。)
1.5.3 具有原型bean依赖项的单例bean #
当您使用依赖项为原型bean的单例bean时,请注意,依赖项是在实例化时处理的。因此,如果将原型bean作为依赖项注入到单例bean中时,那么将实例化一个新的原型bean,然后将它作为依赖项注入到单例bean中。该原型实例是唯一一个提供给单例bean的实例。
但是,假设您希望单例bean在运行时可重复获取原型bean的新实例。那么您不能将原型bean依赖注入到您的单例bean中,因为当Spring容器实例化单例bean并解析和注入其依赖项时,注入只发生一次。如果在运行时多次需要原型bean的新实例,请参阅方法注入。
1.5.4 Request、Session、Application和WebSocket作用域 #
request
、session
、application
和websocket
作用域仅在用于web相关的Spring ApplicationContext
实现(如XmlWebApplicationContext
)中可用。如果将这些作用域用于常规Spring IoC容器(如ClassPathXmlApplicationContext
),则会引发一个关于未知bean作用域的IllegalStateException
。
初始Web配置 #
为了在request
、session
、application
和websocket
级别(web范围的bean)支持bean的作用域,在定义bean之前需要进行一些较小的初始配置。(标准作用域singleton
和prototype
不需要此初始设置。)
如何完成初始设置取决于特定的Servlet环境。
如果您在Spring WebMVC中访问作用域bean,请求实际上是在Spring DispatcherServlet
处理的,则不需要特殊设置。DispatcherServlet
已公开所有相关状态。
如果使用Servlet 2.5 web容器,请求是在Spring的DispatcherServlet
之外处理(例如,当使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener
ServletRequestListener
。对于Servlet 3.0+,这可以通过使用WebApplicationInitializer
接口以编程方式完成。或者,对于旧容器,将以下声明添加到web应用程序的web.xml
文件中:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
或者,如果您的监听器设置存在问题,请考虑使用Spring的RequestContextFilter
过滤器。过滤器映射取决于周围的web应用程序配置,因此必须根据需要进行更改。以下列表展示了web应用程序的过滤器部分:
<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
。这使得请求和会话作用域的bean在调用链的更下游可用。
请求作用域 #
考虑如下bean定义的XML配置:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring容器使用loginAction
bean定义为每个HTTP请求来创建LoginAction
bean的新实例。也就是说,loginAction
bean的作用域是HTTP请求级别的。您可以随意更改所创建实例的内部状态,因为通过同一loginAction
bean定义创建的其他实例看不到这些更改的状态。它们是特定于单个请求的。当请求完成处理时,作用域为请求的bean将被丢弃。
使用注解驱动的组件或Java配置时,@RequestScope
注解可用于将组件分配为request
作用域的。以下示例显示了如何执行此操作:
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
Session作用域 #
考虑如下bean定义的XML配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring容器通过在单个HTTP Session
的生存期内使用userPreferences
bean定义来创建UserPreferences
bean的新实例。换句话说,userPreferences
bean有效地限定了HTTP Session
级别的范围。与作用域为请求的bean一样,您可以根据需要更改所创建实例的内部状态,因为其他HTTP Session
实例也在使用从相同的userPreferences
bean定义创建的实例,它们在状态中看不到这些更改,因为它们是特定于单个HTTP Session
的。当HTTP Session
最终被丢弃时,作用于该特定HTTP Session
的bean也被丢弃。
使用注解驱动组件或Java配置时,可以使用@SessionScope
注解将组件分配为session
作用域。
@SessionScope
@Component
public class UserPreferences {
// ...
}
@SessionScope
@Component
class UserPreferences {
// ...
}
应用作用域 #
考虑以下bean定义的XML配置:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring容器通过使用appPreferences
bean定义来为整个web应用程序创建AppPreferences
bean的新实例。也就是说,appPreferences
bean的作用域是在ServletContext
级别,并作为常规ServletContext
属性存储。这有点类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext
是单例,而不是在每个Spring“ApplicationContext
”中是单例(在任何给定的web应用程序中可能有多个),并且它实际上是公开的,因此作为ServletContext
属性可见。
使用注解驱动组件或Java配置时,可以使用@ApplicationScope
注解将组件分配成application
作用域。以下示例展示了如何执行此操作:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
有作用域的bean作为依赖项 #
Spring IoC容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的注入。如果您想要(例如)将HTTP请求作用域的bean注入到另一个更长寿命作用域的bean中,您可以选择注入AOP代理来代替该作用域的bean。也就是说,您需要注入一个代理对象,该代理对象暴露与作用域对象相同的公共接口,并且也可以从相关作用域(如HTTP请求)检索真实目标对象,并将方法调用委托给真实对象。
您还可以在作用域为
singleton
的bean之间使用<aop:scoped-proxy/>
,然后引用将经过可序列化的中间代理,从而能够在反序列化时重新获取目标单例bean。当针对一个作用域为
prototype
的bean声明<aop:scoped-proxy/>
时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。此外,作用域代理并不是唯一的以生命周期安全的方式从较小作用域访问bean的方法。您还可以将注入点(即构造函数或setter参数或自动注入字段)声明为
ObjectFactory<MyTargetBean>
,允许每次需要时通过getObject()
调用检索当前实例,而无需保留实例或单独存储。作为一个扩展变量,您可以声明
ObjectProvider<MyTargetBean>
,它提供了几个额外的访问变量,包括getIfAvailable
和getIfUnique
。它的JSR-330变体称为
Provider
,它与Provider<MyTargetBean>
声明以及每次检索尝试的相应get()
调用一起使用。有关JSR-330整体的更多详细信息,请参见此处。
以下示例中的配置仅为一行,但了解其背后的“为什么”和“如何做”很重要:
<?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/> 1
</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>
1
|
定义代理的行。 |
要创建这样一个代理,需要将一个子元素<aop:scoped-proxy/>
插入到作用域bean定义中(请参阅选择要创建的代理类型和基于XML模式的配置)。为什么在request
、session
和自定义作用域级别的bean中需要<aop:scoped-proxy/>
元素?考虑下面的单例bean定义,并将它与前面所定义的作用域进行比较(注意,下面的userPreferences
bean定义不完整):
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在前面的示例中,向单例bean(userManager
)中注入了对HTTP Session作用域
bean(userPreferences
)的引用。这里最突出的一点是userManager
是一个单例bean:每个容器只实例化一次,其依赖项(在本例中只有一个,即userPreferences
)也只注入一次。这意味着userManager
bean只在完全相同的userPreferences
对象(即最初注入的对象)上运行。
当将一个寿命较短的作用域bean注入一个寿命较长的作用域bean(例如,将一个HTTP Session
作用域的协作bean作为依赖项注入到单例bean中)时,这不是您想要的行为。相反,您需要一个userManager
对象,并且在HTTP会话的生命周期内,您需要一个特定于该HTTP会话的userPreferences
对象。因此,容器创建了一个对象,该对象暴露了与UserPreferences
类完全相同的公共接口(理想情况下是一个UserPreferences
实例的对象),该对象可以从作用域机制(HTTP请求、会话等)中获取真正的UserPreferences
对象。容器将这个代理对象注入userManager
bean,后者并不知道这个UserPreferences
引用是一个代理。在本例中,当UserManager
实例调用依赖注入的UserPreferences
对象上的方法时,它实际上是在调用代理上的方法。然后,代理从HTTP Session
(在本例中)获取真正的UserPreferences
对象,并将方法调用委托给检索到的真正的UserPreferences
对象。
因此,在将请求范围和会话范围的bean注入协作对象时,需要以下(正确且完整)配置,如下例所示:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
选择要创建的代理的类型 #
默认情况下,当Spring容器为标记有<aop:scoped-proxy/>
元素的bean创建代理时,将创建一个基于CGLIB的类代理。
CGLIB代理只拦截公共方法调用!不要在这样的代理上调用非公共方法。它们不会委托给实际的作用域目标对象。
或者,您可以配置Spring容器,通过将<aop:scoped-proxy/>
元素的proxy-target-class
属性的值指定false,来为此类作用域bean创建基于标准JDK接口的代理。使用基于JDK接口的代理意味着在应用程序类路径中不需要额外的库来实现这种代理。然而,这也意味着作用域bean的类必须实现至少一个接口,并且所有注入作用域bean的协作者必须通过其接口之一引用bean。以下示例显示了基于接口的代理:
<!-- DefaultUserPreferences 实现了 UserPreferences 接口 -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
有关选择基于类或基于接口的代理的更多详细信息,请参阅aop代理。
1.5.5 自定义作用域 #
bean的作用域界定机制是可扩展的。您可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被认为是不好的做法,并且您不能覆盖内置的单例和原型作用域。
创建自定义作用域 #
要将自定义作用域集成到Spring容器中,需要实现org.springframework.beans.factory.config.Scope
接口,本节将对此进行描述。要了解如何实现自己的作用域,请参阅Spring框架本身和Scope
javadoc提供的Scope
实现,其中更详细地解释了需要实现的方法。
Scope
接口有四种方法可以从作用域中获取对象、将它们从作用域中移除、并销毁它们。
例如,会话作用域实现返回会话作用域的bean(如果它不存在,则该方法将一个新的bean实例绑定到会话以供将来使用,然后返回bean实例)。以下方法从基础作用域返回对象:
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,会话作用域实现从基础会话中删除会话作用域bean,则应该返回该对象,但如果找不到具有指定名称的对象,则可以返回null
。以下方法将对象从基础作用域中移除:
Object remove(String name)
fun remove(name: String): Any
以下方法注册作用域销毁或作用域中指定对象销毁时应执行的回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)
有关销毁回调的更多信息,请参阅javadoc或Spring作用域实现。
以下方法获取基础作用域的会话标识符:
String getConversationId()
fun getConversationId(): String
每个作用域的标识符都不同。对于会话作用域的实现,该标识符可以是会话标识符。
使用自定义作用域 #
在编写和测试一个或多个自定义Scope
实现之后,需要让Spring容器知道您的新作用域。以下方法是向Spring容器注册新作用域的核心方法:
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
此方法在ConfigurableBeanFactory
接口上声明的,该接口可通过Spring附带的大多数具体ApplicationContext
实现的BeanFactory
属性获得。
registerScope(..)
方法的第一个参数是与作用域相关的唯一名称。Spring容器本身中此类名称的例子有singleton
和prototype
。registerScope(..)
方法的第二个参数是要注册和使用的自定义作用域实现的实例。
假设编写自定义Scope
实现,然后注册它,如下一个示例所示。
下一个示例使用SimpleThreadScope
,它包含在Spring中,但默认情况下未注册。对于您自己的自定义Scope
实现来说,其做法将是相同的。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)
然后,您可以创建符合自定义Scope
的作用域规则的bean定义,如下所示:
<bean id="..." class="..." scope="thread">
通过自定义Scope
实现,您不仅限于对作用域进行编程注册。还可以使用CustomScopeConfigurer
类声明性地进行作用域注册,如下例所示:
<?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">
<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>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
在FactoryBean
实现中设置<aop:scoped-proxy/>
时,作用域是工厂bean本身,而不是getObject()
返回的对象。