Spring IoC容器:Bean作用域

https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html

当你创建一个bean定义时,你创建了一个创建该bean定义所定义类的实例的配方。将bean定义视为配方的想法很重要,因为这意味着,就像类一样,你可以从单个配方创建多个对象实例。

你不仅可以控制将要插入到从特定bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定bean定义创建的对象的作用域。这种方法强大而灵活,因为你可以通过配置而不是在Java类级别固定对象的作用域来选择你创建的对象的作用域。可以定义Bean在不同的作用域中部署。Spring框架支持六种作用域,其中四种只有在使用支持Web的ApplicationContext时才可用。你也可以创建自定义作用域。

下面描述了支持的作用域:

  • singleton:(默认)将单个bean定义作用域为每个Spring IoC容器的单个对象实例。
  • prototype:将单个bean定义作用域为任意数量的对象实例。
  • request:将单个bean定义作用域为单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己基于单个bean定义创建的bean实例。仅在支持Web的Spring ApplicationContext上下文中有效。
  • session:将单个bean定义作用域为HTTP会话的生命周期。仅在支持Web的Spring ApplicationContext上下文中有效。
  • application:将单个bean定义作用域为ServletContext的生命周期。仅在支持Web的Spring ApplicationContext上下文中有效。
  • websocket:将单个bean定义作用域为WebSocket的生命周期。仅在支持Web的Spring ApplicationContext上下文中有效。

线程作用域是可用的,但默认不注册。

单例作用域(The Singleton Scope)

只管理单例bean的一个共享实例,所有对ID匹配该bean定义的bean的请求都会导致Spring容器返回那一个特定的bean实例。

换句话说,当你定义一个bean定义并将其作用域设置为单例时,Spring IoC容器会创建由该bean定义定义的对象的确切一个实例。这个单一实例存储在这样一个单例bean的缓存中,所有后续对该命名bean的请求和引用都返回缓存对象。下图展示了单例作用域的工作方式:
在这里插入图片描述

Spring对单例bean的概念不同于GoF(Gang of Four)模式书中定义的单例模式。GoF单例硬编码了对象的作用域,以至于每个类加载器只创建一个特定类的实例。Spring单例的范围最好描述为每个容器和每个bean。这意味着,如果你在单个Spring容器中为一个特定类定义了一个bean,Spring容器就会创建由该bean定义定义的类的一个且仅一个实例。单例作用域是Spring中的默认作用域。要在XML中将bean定义为单例,你可以按照以下示例所示定义bean:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

原型作用域(The Prototype Scope)

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 post-processor,该处理器保留需要清理的bean的引用。

在某些方面,Spring容器在原型作用域bean方面的角色是Java new操作符的替代品。所有生命周期管理必须由客户端处理。

单例Bean与原型bean依赖

当你使用单例作用域的bean并依赖原型bean时,请注意依赖关系是在实例化时解析的。因此,如果你将一个原型作用域的bean依赖注入到单例作用域的bean中,会实例化一个新的原型bean并将其依赖注入到单例bean中。这个原型实例是唯一一个提供给单例作用域bean的实例。

然而,假设你希望单例作用域的bean在运行时反复获取原型作用域bean的新实例。你不能将原型作用域的bean依赖注入到你的单例bean中,因为那种注入只会发生一次,即当Spring容器实例化单例bean并解析并注入其依赖关系时。如果你需要在运行时多次需要原型bean的新实例,请参阅方法注入( Method Injection)

请求、会话、应用程序和WebSocket作用域

requestsessionapplicationwebsocket 作用域仅在使用支持Web的Spring ApplicationContext实现(如XmlWebApplicationContext)时可用。如果将这些作用域与常规的Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,将抛出一个表示未知bean作用域的IllegalStateException异常。

初始Web配置

为了支持在requestsessionapplicationwebsocket 级别(Web作用域的bean)对bean进行作用域划分,在定义bean之前需要进行一些轻微的初始配置。(对于标准作用域:singletonprototype,不需要此初始设置。)

如何完成此初始设置取决于你的特定Servlet环境。

如果在Spring Web MVC中访问作用域bean,实际上,在由Spring DispatcherServlet处理的请求中,不需要特别的设置。DispatcherServlet已经暴露了所有相关状态。

如果使用Servlet web容器,并且在Spring的DispatcherServlet之外处理请求(例如,当使用JSF时),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。这可以通过使用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>

DispatcherServletRequestContextListenerRequestContextFilter都是做同样的事情,即将HTTP请求对象绑定到处理该请求的Thread。这使得在调用链中进一步使请求作用域和会话作用域的bean可用。

请求作用域(Request scope)

考虑以下XML配置用于bean定义:

<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 {
	// ...
}

会话作用域(Session Scope)

考虑以下XML配置用于bean定义:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Spring容器使用userPreferences bean定义为单个HTTP会话的生命周期创建一个UserPreferences bean的新实例。换句话说,userPreferences bean实际上在HTTP会话级别进行作用域划分。与请求作用域的bean一样,可以根据需要更改创建的实例的内部状态,因为其它HTTP会话实例也在使用从相同的userPreferences bean定义创建的实例,但它们不会看到这些状态变化,因为这些变化特定于单个HTTP会话。当HTTP会话最终被丢弃时,作用域为该特定HTTP会话的bean也将被丢弃。

当使用注解驱动的组件或Java配置时,可以使用@SessionScope注解将组件分配给session 作用域。

@SessionScope
@Component
public class UserPreferences {
	// ...
}

应用程序作用域(Application Scope)

考虑以下XML配置用于bean定义:

<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 {
	// ...
}

WebSocket 作用域

WebSocket作用域与WebSocket会话的生命周期相关联,并适用于基于WebSocket的STOMP应用程序。

作用域Bean作为依赖项(Scoped Beans as Dependencies)

Spring IoC容器不仅管理对象(bean)的实例化,还负责连接协作者(或依赖项)。如果您想要将一个HTTP请求作用域的bean注入到另一个生命周期更长的bean中,可以选择注入一个AOP代理来代替作用域bean。也就是说,你需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但它也可以从相关的作用域(如HTTP请求)中检索实际的目标对象,并将方法调用委托给实际的对象。

还可以在作用域为singleton的bean之间使用<aop:scoped-proxy/> ,这样引用将通过一个可序列化的中间代理,从而能够在反序列化时重新获取目标单例bean。

在针对作用域为原型的bean声明<aop:scoped-proxy/>时,对共享代理的每次方法调用都会导致创建一个新的目标实例,然后调用被转发到该实例。

此外,作用域代理并不是以生命周期安全的方式从较短作用域访问bean的唯一方法。还可以将注入点(即构造函数或setter参数或自动装配字段)声明为ObjectFactory<MyTargetBean>,允许在每次需要时通过getObject()调用按需检索当前实例——而不需要持有实例或单独存储。

作为一个扩展变体,可以声明ObjectProvider<MyTargetBean>,它提供了几种额外的访问变体,包括getIfAvailablegetIfUnique

这个JSR-330的变体称为Provider,使用Provider声明和相应的get()调用进行每次检索尝试。

以下示例中的配置只有一行,但理解其背后的“为什么”以及“如何”是很重要的:

<?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>

上面<aop:scoped-proxy/>这行定义了proxy。要创建这样的代理,你需要将子元素<aop:scoped-proxy/> 插入到作用域bean定义中。

为什么在常见场景中,requestsession 和自定义作用域级别的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会话作用域bean(userPreferences)的引用。这里的关键点是userManager bean是一个单例:它每个容器只实例化一次,它的依赖项(在这个例子中只有一个,即userPreferences bean)也只注入一次。这意味着userManager bean只在完全相同的userPreferences对象上操作(即,最初注入的那个)。

当将生命周期较短的作用域bean注入到生命周期较长的作用域bean时(例如,将HTTP会话作用域的协作bean作为依赖项注入到单例bean中),这不是您想要的行为。相反,你需要一个userManager对象,在HTTP会话的生命周期内,需要一个特定于HTTP会话的userPreferences对象。因此,容器创建了一个暴露与UserPreferences类完全相同的公共接口的对象(理想情况下是一个UserPreferences实例),该对象可以从作用域机制(HTTP请求、会话等)获取真正的UserPreferences对象。容器将此代理对象注入到userManager bean中,userManager bean并不知道这个UserPreferences引用是一个代理。在这个例子中,当UserManager实例在依赖注入的UserPreferences对象上调用方法时,实际上是在代理上调用方法。然后代理从(在这个例子中是)HTTP会话中获取真正的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代理不会拦截私有方法。尝试在这样的代理上调用私有方法不会委托给实际的作用域目标对象。

或者,可以通过为<aop:scoped-proxy/>元素的proxy-target-class属性指定false来配置Spring容器为这样的作用域bean创建标准的JDK基于接口的代理。使用JDK基于接口的代理意味着你的应用程序类路径中不需要额外的库来实现这样的代理。然而,这也意味着作用域bean的类必须至少实现一个接口,并且所有注入作用域bean的协作者都必须通过其接口之一引用该bean。以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<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>

直接注入请求/会话引用

作为工厂作用域的替代方案,Spring WebApplicationContext还支持将HttpServletRequestHttpServletResponseHttpSessionWebRequest以及(如果存在JSF)FacesContextExternalContext注入到Spring管理的bean中,只需通过基于类型的自动装配,与为其它bean的常规注入点相邻。Spring通常会为这些请求和会话对象注入代理,这有一个优点,即在单例bean和可序列化bean中也能正常工作,类似于工厂作用域bean的作用域代理。

定义作用域

bean作用域机制是可扩展的。可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被认为是不好的实践,你不能覆盖内置的singletonprototype作用域。

创建自定义作用域

要将自定义作用域集成到Spring容器中,需要实现org.springframework.beans.factory.config.Scope接口。要了解如何实现自己的范围,请参阅随Spring框架本身提供的Scope实现以及Scope javadoc。

Scope接口有四个方法,用于从作用域中获取对象、从作用域中移除对象以及让它们被销毁。

例如,会话作用域实现返回会话作用域bean(如果它不存在,该方法在将bean绑定到会话以供将来引用后返回bean的新实例)。以下方法从底层作用域返回对象:

Object get(String name, ObjectFactory<?> objectFactory)

例如,会话作用域的实现会从底层会话中移除会话作用域的bean。应该返回对象,但如果找不到指定名称的对象,可以返回null。以下方法从底层作用域中移除对象:

Object remove(String name)

以下方法注册了一个回调,当作用域被销毁或者作用域中的指定对象被销毁时,作用域应该调用这个回调:

void registerDestructionCallback(String name, Runnable destructionCallback)

以下方法获取底层作用域的对话标识符:

String getConversationId()

这个标识符对于每个作用域都是不同的。对于会话作用域的实现,这个标识符可以是会话标识符。

使用自定义作用域

在编写并测试了一个或多个自定义Scope 实现之后,需要让Spring容器知道新作用域。以下方法是向Spring容器注册新作用域的核心方法:

void registerScope(String scopeName, Scope scope);

这个方法在ConfigurableBeanFactory接口上声明,可以通过大多数随Spring附带的具体ApplicationContext实现上的BeanFactory属性获得。

registerScope(..)方法的第一个参数是与作用域关联的唯一名称。在Spring容器本身中,这样的名字的例子有singletonprototyperegisterScope(..)方法的第二个参数是希望注册并使用的实际自定义作用域实现的实例。

下面的例子使用了SimpleThreadScope,它包含在Spring中,但默认情况下不注册。对于你自己的自定义作用域实现,操作说明是相同的。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

然后,你可以创建遵循自定义Scope的scoping规则的bean定义,如下所示:

<bean id="..." class="..." scope="thread">

使用自定义作用域实现,你不仅限于以编程方式注册作用域。还可以通过使用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()返回的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值