您不仅可以控制要插入到从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围。这种方法功能强大且灵活,因为您可以通过配置选择您创建的对象的范围,而不必在 Java 类级别烘焙对象的范围。可以将 Bean 定义为部署在多个范围之一中。Spring Framework 支持六个范围,其中四个仅在您使用 web-aware 时才可用ApplicationContext
。您还可以创建 自定义范围。
下表描述了支持的范围:
范围 | 描述 |
---|---|
singleton | (默认)将单个 bean 定义范围限定为每个 Spring IoC 容器的单个对象实例。 |
prototype | 将单个 bean 定义范围限定为任意数量的对象实例。 |
request | 将单个 bean 定义范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该 bean 实例是在单个 bean 定义的后面创建的。仅在 web-aware Spring 的上下文中有效ApplicationContext 。 |
session | 将单个 bean 定义范围限定为 HTTP 的生命周期Session 。仅在 web-aware Spring 的上下文中有效ApplicationContext 。 |
application | 将单个 bean 定义范围限定为ServletContext . 仅在 web-aware Spring 的上下文中有效ApplicationContext 。 |
websocket | 将单个 bean 定义范围限定为WebSocket . 仅在 web-aware Spring 的上下文中有效ApplicationContext 。 |
只有一个单例 bean 的共享实例被管理,并且所有对带有一个或多个 ID 与该 bean 定义匹配的 bean 的请求都会导致 Spring 容器返回一个特定的 bean 实例。
换句话说,当您定义一个 bean 定义并且它的作用域是一个单例时,Spring IoC 容器会创建该 bean 定义定义的对象的一个实例。该单个实例存储在此类单例 bean 的缓存中,并且对该命名 bean 的所有后续请求和引用都返回缓存对象。
Spring 的单例 bean 概念不同于四人组 (GoF) 模式书中定义的单例模式。GoF 单例对对象的范围进行了硬编码,以便每个 ClassLoader 只创建一个特定类的一个实例。Spring 单例的范围最好描述为每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会创建该 bean 定义定义的类的一个且仅一个实例。单例作用域是 Spring 中的默认作用域。
<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"/>
bean 部署的非单一原型范围导致每次对特定 bean 发出请求时都会创建一个新 bean 实例。也就是说,bean 被注入到另一个 bean 中,或者您通过getBean()
容器上的方法调用来请求它。通常,您应该对所有有状态 bean 使用原型作用域,对无状态 bean 使用单例作用域。
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域相比,Spring 不管理原型 bean 的完整生命周期。容器实例化、配置和以其他方式组装原型对象并将其交给客户端,没有该原型实例的进一步记录。因此,尽管在所有对象上调用初始化生命周期回调方法,而不管范围如何,但在原型的情况下,不会调用配置的销毁生命周期回调。客户端代码必须清理原型范围内的对象并释放原型 bean 持有的昂贵资源。要让 Spring 容器释放原型作用域 bean 持有的资源,请尝试使用自定义bean post-processor,它保存对需要清理的 bean 的引用。
在某些方面,Spring 容器在原型作用域 bean 方面的角色是 Javanew
运算符的替代品。超过该点的所有生命周期管理都必须由客户端处理。(有关 Spring 容器中 bean 生命周期的详细信息,请参阅生命周期回调。)
当您使用具有对原型 bean 的依赖的单例作用域 bean 时,请注意在实例化时解析依赖关系。因此,如果您将原型范围的 bean 依赖注入到单例范围的 bean 中,则会实例化一个新的原型 bean,然后将依赖项注入到单例 bean 中。原型实例是唯一提供给单例作用域 bean 的实例。
但是,假设您希望单例范围的 bean 在运行时重复获取原型范围的 bean 的新实例。您不能将原型范围的 bean 依赖注入到您的单例 bean 中,因为该注入仅发生一次,当 Spring 容器实例化单例 bean 并解析并注入其依赖项时。如果您在运行时多次需要原型 bean 的新实例,请参阅方法注入。
只有当您使用webAware的Spring ApplicationContext实现(如XmlWebApplicationContext)时,request、session、application和websocket作用域才可用。如果将这些作用域与常规Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,则会抛出一个举报未知bean作用域的IllegalStateException。
为了在request、session、application和websocket级别(web作用域bean)支持bean的作用域,在定义bean之前需要进行一些小的初始配置。(这种初始设置对于标准作用域是不需要的:singleton和prototype。)
如何完成这个初始设置取决于特定的Servlet环境。
如果您访问Spring Web MVC中的作用域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容器通过为每个HTTP请求使用LoginAction bean定义来创建LoginAction bean的新实例。
也就是说,loginAction bean的作用域是在HTTP请求级别。您可以随心所欲地更改已创建实例的内部状态,因为从相同的loginAction bean定义创建的其他实例看不到这些状态更改。
它们是针对个别要求的。当请求完成处理时,将丢弃作用域为该请求的bean。
当使用注释驱动的组件或Java配置时,可以使用@RequestScope注释将组件分配到请求范围。
下面的例子展示了如何做到这一点:
@RequestScope @Component public class LoginAction { // ... }
考虑 bean 定义的以下 XML 配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring容器通过在单个HTTP会话的生命周期中使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean有效地限定在HTTP会话级别。与请求范围内bean一样,你可以改变内部状态的实例创建尽可能多的你想要的,知道其他HTTP会话实例也使用相同的实例创建userPreferences bean定义看不到这些变化状态,因为他们是特定于一个单独的HTTP会话。当HTTP Session最终被丢弃时,限定在该特定HTTP Session范围内的bean也将被丢弃。
当使用注释驱动的组件或Java配置时,您可以使用@SessionScope注释将组件分配到会话作用域。
@SessionScope @Component public class UserPreferences { // ... }
考虑 bean 定义的以下 XML 配置:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring容器通过对整个web应用使用一次AppPreferences bean定义来创建一个AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在ServletContext级别,并存储为常规的ServletContext属性。这有点类似于弹簧单例bean,但在两个重要方面不同:它是一个单例每ServletContext不是每Spring ApplicationContext(可能有几个在任何给定的web应用程序),它实际上是暴露,因此可见ServletContext属性。
当使用注释驱动的组件或Java配置时,您可以使用@ApplicationScope注释将组件分配给应用程序范围。
下面的例子展示了如何做到这一点:
@ApplicationScope @Component public class AppPreferences { // ... }
Spring IoC容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的连接。如果您想将(例如)一个HTTP请求作用域的bean注入到另一个寿命较长的作用域bean中,您可以选择注入一个AOP代理来代替该作用域bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(例如HTTP请求)检索实际目标对象,并将方法调用委托给实际对象。
注意:
您还可以在作用域为单例的bean之间使用<aop:作用域代理/>
,然后引用通过一个可序列化的中间代理,从而能够在反序列化时重新获得目标单例bean。
当对作用域原型bean声明<aop:作用域代理/>
时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该目标实例。
而且,限定范围的代理并不是以生命周期安全的方式从较短范围访问bean的唯一方法。
你也可以声明你的注射点(也就是构造函数或setter参数或autowired的字段)作为ObjectFactory < MyTargetBean >,允许getObject()调用来检索当前实例对需求每次需要——没有分别持有实例或存储它。
作为扩展变量,您可以声明ObjectProvider
它的JSR-330变体称为Provider,与Provider
下面示例中的配置只有一行,但是理解“为什么”和“如何”是很重要的:
<?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>
The line that defines the proxy.
要创建这样的代理,将子元素<aop:作用域代理/>
插入作用域bean定义(参见选择要创建的代理类型和基于XML schema配置 xsd模式)。为什么作用域在' request '、' session '和自定义作用域级别的bean定义需要<aop:作用域代理/>
元素?考虑下面的单例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”bean)也只注入一次。这意味着“userManager”bean只对完全相同的“userPreferences”对象(即最初注入它的对象)进行操作。
当将寿命较短的作用域bean注入寿命较长的作用域bean时(例如,将HTTP“Session”作用域协作bean作为依赖项注入到单例bean中),这不是您想要的行为。相反,您需要一个单一的' userManager '对象,并且,对于HTTP ' Session '的生命周期,您需要一个特定于HTTP ' Session '的' userPreferences '对象。因此,容器创建了一个对象,它暴露了与“UserPreferences”类完全相同的公共接口(理想情况下是一个“UserPreferences”实例对象),该对象可以从范围机制(HTTP请求、“Session”等)中获取真正的“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容器为这些作用域bean创建标准的基于JDK接口的代理,方法是为<aop:scoped-proxy/>
元素的“proxy-target-class”属性的值指定' false '。
使用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>
有关选择基于类或基于接口的代理的更多详细信息,请参阅代理机制。
bean作用域机制是可扩展的。您可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被认为是不好的做法,而且您不能覆盖内置的单例和原型作用域。
要将您的自定义作用域集成到Spring容器中,您需要实现org.springframework.beans.factory.config.Scope接口,该接口将在本节中描述。要了解如何实现自己的作用域,请参阅Spring框架本身提供的作用域实现和Scope javadoc,后者更详细地解释了需要实现的方法。
Scope接口有四个方法来从作用域获取对象、从作用域删除对象和销毁对象。
例如,会话范围实现返回会话范围的bean(如果它不存在,则方法将其绑定到会话以供将来引用后返回该bean的新实例)。下面的方法从底层作用域返回对象:
Object get(String name, ObjectFactory<?> objectFactory)
例如,会话范围实现从底层会话中删除会话范围的bean。应该返回对象,但是如果没有找到指定名称的对象,可以返回null。下面的方法将对象从底层作用域中移除:
Object remove(String name)
以下方法注册了一个回调,当它被销毁或范围中的指定对象被销毁时,该范围应该调用该回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
以下方法获取基础范围的对话标识符:
String getConversationId()
这个标识符对于每个范围都是不同的。对于会话范围的实现,此标识符可以是会话标识符。
在编写和测试一个或多个自定义作用域实现之后,需要让Spring容器知道您的新作用域。
下面的方法是向Spring容器注册一个新的Scope的中心方法:
void registerScope(String scopeName, Scope scope);
他的方法是在ConfigurableBeanFactory接口上声明的,该接口可以通过Spring附带的大多数具体ApplicationContext实现上的BeanFactory属性获得。
registerScope(..)方法的第一个参数是与作用域关联的唯一名称。Spring容器中此类名称的例子有singleton和prototype。registerScope(..)方法的第二个参数是您希望注册和使用的自定义Scope实现的实际实例。
假设您编写了自定义Scope实现,然后按下一个示例所示注册它。
注意:
下一个示例使用SimpleThreadScope,它包含在Spring中,但默认情况下没有注册。对于您自己的自定义Scope实现,说明是相同的。
Scope threadScope = new SimpleThreadScope(); beanFactory.registerScope("thread", threadScope);
然后,您可以创建遵循自定义Scope范围规则的bean定义,如下所示:
<bean id="..." class="..." scope="thread">
使用自定义范围实现,您将不再局限于范围的程序化注册。您还可以通过使用CustomScopeConfigurer类声明地进行Scope注册,如下例所示:
<?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实现的