接上篇:Spring 核心技术(5)
version 5.1.8.RELEASE
创建 bean 定义时,你创建了一种用于创建 bean 定义中定义的类实例的方法。bean定义的设想是一个很重要的方法,因为它意味着,与一个类一样,你可以从以一种方式创建许多对象实例。
你不仅可以控制要插入到以特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制以特定bean定义创建的对象的作用域。这种方法功能强大且灵活,因为你可以选择通过配置创建的对象的作用域,而不必在 Java 类级别设定对象的作用域。Bean 可以被部署到定义的多个作用域之一中。Spring Framework 支持六个作用域,其中四个范围仅在使用支持 Web 的 ApplicationContext
时可用。你还可以创建 自定义范围
。
下表描述了支持的作用域:
作用域 | 描述 |
---|---|
单例 | (默认)将单个 bean 定义的作用域限定为每个 Spring IoC 容器的单个对象实例。 |
原型 | 将单个 bean 定义作用域限定为任意数量的对象实例。 |
请求 |
将单个 bean 定义作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,它是在单例 bean 定义的后面创建的。仅在支持 web 的 Spring ApplicationContext
中可用。
会话
|将单个 bean 定义作用域限定为 HTTP 的生命周期 Session
。仅在支持 web 的 Spring ApplicationContext
中可用。
应用
|将单个 bean 定义作用域限定为 ServletContext
的生命周期。仅在支持 web 的 Spring ApplicationContext
中可用。
websocket
|将单个 bean 定义作用域限定为 WebSocket
的生命周期。仅在支持 web 的 Spring ApplicationContext
中可用。
从 Spring 3.0 开始,线程作用域可用,但默认情况下不会注册:请参阅 SimpleThreadScope 。从 Spring 4.2 开始,事务作用域可用:请参阅 SimpleTransactionScope 。有关如何注册这些或任何其他自定义作用域的说明,请参阅 使用自定义作用域 。
只管理单个 bean 的一个共享实例,并且对具有与该 bean 定义匹配的 ID 的所有请求都会导致 Spring 容器返回一个特定的 bean 实例。
换句话说,当你定义了一个 bean 定义并将其作用域设置为单例时,Spring IoC 容器只会通过该 bean 定义创建一个实例。此单个实例存储在此类单例 bean 的缓存中,并且该命名 Bean 的所有后续请求和引用都将返回此缓存对象。下图显示了单例作用域的工作原理:
Spring 的单例 bean 概念不同于 Gang of Four(GoF)设计模式书中定义的单例模式。GoF 单例对一个对象的作用域进行硬编码,使得每个 ClassLoader 只能创建一个特定类的实例。Spring 单例的作用域最好描述为一个容器一个 bean。这意味着,如果在 Spring 容器中定义一个 bean 为单例,则 Spring 容器只会根据该 bean 定义创建一个实例。单例作用域是 Spring 的默认作用域。要在 XML 中将 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"/>
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 的生命周期的详细信息,请参阅 生命周期回调
。)
当使用依赖于原型 bean 的单例作用域 bean 时,请注意依赖项是在实例化时解析。因此,如果通过依赖注入将原型作用域的 bean 注入到单例作用域的 bean 中,则会实例化一个新的原型 bean,然后将依赖注入到单例 bean 中。原型实例是唯一可以提供给单例作用域 bean 的实例。
但是,假如你希望单例作用域的 bean 在运行时重复获取原型作用域的 bean 的新实例。那么不能将原型作用域的 bean 依赖注入到单例 bean 中,因为该注入过程只在 Spring 容器实例化单例 bean 并解析注入其依赖项时发生一次。如果需要在运行时多次使用原型 bean 的新实例,请参阅 方法注入
request
, session
, application
和 websocket
作用域只有当你使用一个支持 web 的 Spring ApplicationContext
实现(例如 XmlWebApplicationContext
)时可用。如果将这些作用域与常规的 Spring IoC 容器一起使用,例如 ClassPathXmlApplicationContext
,将会因为未知 bean 作用域导致抛出 IllegalStateException
异常。
为了支持 bean 在 request
, session
, application
和 websocket
级别的作用域(即具有 web 作用域 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 可以在调用链的下游进一步使用。
请看以下使用 XML 配置的 bean 定义:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器通过为每个 HTTP 请求使用 loginAction
定义创建一个 LoginAction
的新实例。也就是说, loginAction
的作用域是 HTTP 请求级别。你可以根据需要更改创建的实例的内部状态,因为同样从 loginAction
定义创建的其他实例看不到在状态中的更改。它们特别针对个别要求。当请求完成处理时,作用于该请求的 bean 将会被废弃。
使用注解驱动的组件或 Java 配置时, @RequestScope
注释可用于将组件分配给 request
作用域。以下示例显示了如何执行此操作:
@RequestScope @Component public class LoginAction { // ... }
请看以下使用 XML 配置的 bean 定义:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器通过使用 userPreferences
定义为单个 HTTP Session
创建一个新的 UserPreferences
实例。换句话说, userPreferences
bean 的有效作用域是 HTTP Session
级别。与 request
作用域的 bean 一样,你可以根据需要更改创建的实例的内部状态,因为知道同样使用 userPreferences
bean定义创建的其他 HTTP Session
实例看不到这些状态上的更改,因为它们特定于单个HTTP Session
。当 Session
最终销毁时,同时也会销毁作用于该特定 HTTP Session
的 bean。
使用注解驱动的组件或 Java 配置时,可以使用 @SessionScope
注解将组件指定为 session
作用域。
@SessionScope @Component public class UserPreferences { // ... }
请看以下使用 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 { // ... }
Spring IoC 容器不仅管理对象(bean)的实例化,还管理协作者(或依赖关系)的关联。如果要将(例如)HTTP 请求作用域的 bean 注入到作用域范围更大的另一个 bean 中,你可以选择注入 AOP 代理来代替作用域 bean。也就是说,你需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)中找到真实目标对象,并将方法调用委托给真实对象。
你还可以在单例作用域的 bean 中使用 <aop:scoped-proxy/>
,然后通过可序列化的中间代理进行引用,从而能够在反序列化时重新获取目标单例 bean。
当对原型作用域的 bean 声明 <aop:scoped-proxy/>
时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后转发该调用。
此外,作用域代理不是以生命周期安全的方式从较小作用域中访问 bean 的唯一方法。你还可以将注入点(即构造函数或 setter 参数或 autowired 字段)声明为 ObjectFactory<MyTargetBean>
, 允许在每次有需要时调用 getObject()
找回当前实例,而无需保留实例或单独存储它。
作为扩展的变体,你可以声明 ObjectProvider<MyTargetBean>
,它提供了几个额外的访问变体,包括 getIfAvailable
和 getIfUnique
。
JSR-330 变量被称作 Provider,每次尝试调用相应的 get()
时可通过 Provider<MyTargetBean>
定义使用。有关 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/> ① </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/>
作为子元素插入到作用域 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>
在前面的示例中,HTTP Session
级作用域的 bean ( userPreferences
) 被作为依赖项注入到单例 bean ( userManager
)。这里的重点是 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中,该 bean 并不知道此 UserPreferences
引用的是代理。在这个例子中,当一个 UserManager
实例调用依赖注入的 UserPreferences
对象上的一个方法,它实际上是在代理上调用一个方法。然后,代理会从 HTTP Session
中获取实际的 UserPreferences
对象(在此例子中),并将方法调用委派给获取到的真实对象。
因此,在将 request 和
session` 作用域的 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 的所有协作者必须通过其中一个接口引用它。以下示例显示了基于接口的代理:
<!-- 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 范围机制是可扩展的。你可以定义自己的范围,甚至可以重新定义现有范围,后者并不是很好的做法,而且你无法覆盖内置 singleton 和 prototype 范围。
要将自定义作用域集成到 Spring 容器中,需要实现本节中描述的 org.springframework.beans.factory.config.Scope
接口。有关如何实现自己的作用域的主意,请参阅 Spring Framework 本身和 javadoc 提供的 Scope
实现 ,它们解释了你需要实现的方法的细节。
Scope
接口有四种方法,可以从作用域中获取对象,将其从作用域中删除,然后将其销毁。
例如,会话作用域实现类返回会话范围的 bean(如果它不存在,则该方法在将其绑定到会话以供后续引用之后返回该 bean 的新实例)。以下方法从基础范围返回对象:
Object get(String name, ObjectFactory objectFactory)
例如,会话作用域实现类实现从基础会话中删除会话作用域的 bean。应返回该对象,但如果找不到具有指定名称的对象,则可以返回 null。以下方法从基础范围中删除对象:
Object remove(String name)
以下方法记录作用域在销毁或作用域中指定对象被销毁时应执行的回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
有关销毁回调的更多信息,请参阅 javadoc 或 Spring 作用域实现。
以下方法获取基础作用域的会话标识符:
String getConversationId()
每个作用域的标识符都不同。对于会话作用域的实现,该标识符可以是会话的标识符。
在编写并测试一个或多个自定义 Scope
实现之后,你需要让 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);
然后,您可以创建符合自定义的作用域规则的 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()
返回的对象。