当你创建一个bean
的定义时候,你可以创建一个模版(recipe)通过bean
定义的类定义去创建一个真实的实例。bean
定义是模版(recipe)的概念很重要,因为这意味着,与使用类一样,你可以从一个模版(recipe)创建多个对象实例。
你不仅可以控制要插入到从特定bean
定义创建的对象中的各种依赖项和配置值,还可以控制从特定bean
定义创建的对象的作用域。这种方法是非常有用的和灵活的,因为你可以选择通过配置创建的对象的作用域,而不必在Java类级别上考虑对象的作用域。bean
能够定义部署到一个或多个作用域。Spring
框架支撑6种作用域,4种仅仅使用web
环境。你可以创建定制的作用域。
下面的表格描述了支撑的作用域:
Scope | Description |
---|---|
singleton | (默认)将每个Spring IoC容器的单个bean定义范围限定为单个对象实例。 |
prototype | 将单个bean定义的作用域限定为任意数量的对象实例 |
request | 将单个bean定义的范围限定为单个HTTP请求的生命周期。也就是,每个HTTP请拥有一个被创建的bean实例。仅在Spring ApplicationContext Web容器有效 |
session | 将单个bean定义的范围限制在HTTP Session生命周期。仅在Spring ApplicationContext Web容器有效 |
application | 将单个bean定义的范围限制在ServletContext生命周期。仅在Spring ApplicationContext Web容器有效 |
websocket | 将单个bean定义限制在WebSocket生命周期。仅在Spring ApplicationContext Web容器有效 |
从Spring3.0
后,线程安全作用域是有效的但默认没有注册。更多的信息,查看文档 SimpleThreadScope
。更多关于怎样去注册和自定义作用域,查看自定义作用域
1.5.1 单例bean作用域
单例bean
仅仅只有一个共享实例被容器管理,并且所有对具有与该bean
定义相匹配的ID
的bean
的请求都会导致该特定bean
实例被Spring
容器返回。换一种方式,当你定义一个bean
的定义并且它的作用域是单例的时候,Spring IoC
容器创建通过bean
定义的对象定义的实例。这个单例存储在缓存中,并且对命名bean
的所有请求和引用返回的是缓存对象。下面图片展示了单例bean
作用域是怎样工作的:
Spring
的单例bean
概念与在GoF
设计模式书中的单例模式不同。GoF
单例硬编码对应的作用域例如:只有一个特定类的对象实例对每一个ClassLoader
只创建一个对象实例。最好将Spring
单例的范围描述为每个容器和每个bean
(备注:GoF
设计模式中的单例bean
是针对不同ClassLoader
来说的,而Spring
的单例是针对不同容器级别的)。这意味着,如果在单个Spring
容器对指定类定义一个bean
,Spring
容器通过bean
定义的类创建一个实例。在Spring
中单例作用域是默认的。在XML中去定义一个bean
为单例,你可以定义一个bean
类似下面例子:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 通过scope指定bean作用域 单例:singleton ,原型:prototype-->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2 原型作用域
非单例原型bean
的作用域部署结果是在每一次请求指定bean
的时候都会创建一个bean
实例。也就是,bean
被注入到其他bean
或在容器通过getBean()
方法调用都会创建一个新bean
。通常,为所有的无状态bean使用原型作用域并且有状态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
运算符的替代。所有超过该点的生命周期管理都必须由客户端处理。(更多关于在Spring
容器中的bean
生命周期,查看生命周期回调)
1.5.3 单例bean与原型bean的依赖
当你使用依赖于原型bean
的单例作用域bean
时(单例引用原型bean
),需要注意的是这些依赖项在初始化时候被解析。因此,如果你依赖注入一个原型bean
到一个单例bean
中,一个新原型bean
被初始化并且依赖注入到一个单例bean
。原型实例是唯一一个被提供给单例作用域bean
的实例。(备注:单例引用原型bean时原型bean只会有一个)
然而,假设你希望单例作用域bean
在运行时重复获取原型作用域bean
的一个新实例。你不能依赖注入一个原型bean
到一个单例bean
,因为注入只发生一次,当Spring
容器实例化单例bean
、解析和注入它的依赖时。如果在运行时不止一次需要原型bean
的新实例,查看方法注入
1.5.4 Request, Session, Application, and WebSocket Scopes
request
、session
、application
、和websocket
作用域仅仅在你使用Spring
的ApplicationContext
实现(例如:XmlWebApplicationContext
)时有效。如果你将这些作用域与常规的Spring IoC
容器(例如ClassPathXmlApplicationContext
)一起使用,则会抛出一个IllegalStateException
异常,该错抛出未知的bean
作用域。
-
初始化Web配置
为了支持这些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
或Structs
),你需要去注册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
应用程序filter
的部分配置:<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
请求对象绑定到为该请求提供服务的线程。这使得request
和session
范围的bean在调用链的更下方可用。 -
Request
作用域考虑下面的XML关于
bean
的定义:<!--请求作用域为request--> <bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring
容器通过使用LoginAction
bean定义为每个HTTP
的请求创建一个LoginAction
新实例bean。也就是说,loginAction
bean的作用域在HTTP
请求级别。你可以根据需要更改所创建实例的内部状态。因为从同一loginAction
bean定义创建的其他实例看不到状态的这些变化。当这个请求处理完成,bean的作用域从request
丢弃。(备注:scope="request"
每个请求是线程级别隔离的、互不干扰)当使用注解驱动组件或
Java Config
时,@RequestScope
注解能够赋值一个组件到request
作用域。下面的例子展示怎样使用:@RequestScope//指定作用域访问为request @Component public class LoginAction { // ... }
-
Session作用域
考虑下面为
bean
定义的XML
配置:<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring
容器通过使用userPreferences
的bean定义为单个HTTP
Session
的生命周期内的创建一个UserPreferences
的新实例。换句话说,userPreferences
bean有效地作用在HTTP会话级别。与请求范围的Bean一样,您可以根据需要任意更改所创建实例的内部状态,因为知道其他也在使用从同一``userPreferencesBean定义创建的实例的HTTP Session实例也看不到这些状态变化,因为它们特定于单个HTTP会话。当
HTTP会话最终被丢弃时,作用于该特定
HTTP会话的
bean`也将被丢弃。当使用注解驱动组件或
Java Config
时,@SessionScope
注解能够赋值一个组件到session
作用域。下面的例子展示怎样使用:@SessionScope @Component public class UserPreferences { // ... }
-
Application作用域
考虑下面的XML关于
bean
的定义:<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring
容器通过使用appPreferences
的bean定义为整个Web
应用创建一个AppPreferences
的bean新实例。也就是说,appPreferences
的作用域在ServletContext
级别并且作为一个常规的ServletContext
属性被储存。这个和Spring
的单例bean类似,但有两个重要的区别:每个ServletContext
是一个单例,而不是每个Spring
的ApplicationContext
(在给定的Web
应用程序中可能有多个),并且它实际上是暴露的,因此作为ServletContext
属性可见。当使用注解驱动组件或
Java Config
时,@ApplicationScope
注解能够赋值一个组件到application
作用域。下面的例子展示怎样使用:@ApplicationScope @Component public class AppPreferences { // ... }
-
作用域bean作为依赖项
Spring IoC容器不仅管理对象(bean)的实例化,而且还管理协同者(或依赖项)的连接。(例如)如果要将HTTP请求范围的Bean注入另一个作用域更长的Bean,则可以选择注入AOP代理来代替已定义范围的Bean。也就是说,你需要注入一个代理对象,该对象暴露与范围对象相同的公共接口,但也可以从相关范围(例如HTTP请求)中检索实际目标对象,并将方法调用委托给实际对象。
在这些
bean
作用域是单例之间,你可以使用<aop:scoped-proxy/>
。然后通过一个可序列化的中间代理引用,从而能够在反序列化时重新获得目标单例bean
。当申明
<aop:scoped-proxy/>
原型作用域bean
,每个方法调用共享代理导致一个新目标实例被创建,然后将该调用转发到该目标实例。同样,作用域代理不是以生命周期安全的方式从较短的作用域访问
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"> <!-- 一个HTTP Session作用域的bean暴露为一个代理 --> <bean id="userPreferences" class="com.something.UserPreferences" scope="session"> <!-- 指示容器代理周围的bean --> <aop:scoped-proxy/> //1. </bean> <!-- 一个单例作用域bean 被注入一个上面的代理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
定义中(查看选择代理类型去创建 和 基于Schema的XML配置)。为什么这些bean
的定义在request
、session
和自定义作用域需要<aop:scoped-proxy/>
元素?考虑以下单例bean
定义,并将其与需要为上述范围定义的内容进行对比(请注意,以下userPreferences
bean定义不完整):
<!--没有<aop:scoped-proxy/> 元素-->
<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是一个单例bean:这个实例在每个容器值初始化一次,并且它的依赖(在这个例子仅仅一个,userPreferences
bean)仅仅被注入一次。这意味着userManager
bean运行仅仅在相同的userPreferences
对象上(也就是,最初注入的那个)。
当注入一个短生命周期作用域的bean
到一个长生命周期作用域bean
的时候这个不是我们期望的方式(例如:注入一个HTTP Session
作用域的协同者bean
作为一个依赖注入到单例bean
)。相反,你只需要一个userManager
对象,并且在HTTP
会话的生存期内,你需要一个特定于HTTP会话的userPreferences
对象。因此,容器创建一个对象,该对象公开与UserPreferences
类完全相同的公共接口(理想地,对象是UserPreferences
实例),可以从作用域机制(HTTP 请求,Session
,以此类推)获取真正的UserPreferences
对象。容器注入这个代理对象到userManager
bean,这并不知道此UserPreferences
引用是代理。在这个例子中,当UserManager
实例调用在依赖注入UserPreferences
对象上的方法时,它实际上是在代理上调用方法。然后代理从(在本例中)HTTP
会话中获取实际的UserPreferences
对象,并将方法调用委托给检索到的实际UserPreferences
对象。
因此,在将request-scoped
和session-scoped
的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
容器为bean
创建一个代理,这个bean
通过<aop:scoped-proxy/>
元素被标记,基于CGLIB
的类代理被创建。CGLIB
代理拦截器仅仅公共方法被调用!在代理上不要调用非公共方法。或者,你可以为作用域
bean
配置Spring
容器创建标准的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>
更多详细信息关于选择基于
class
或基于接口代理,参考代理机制参考代码:
com.liyong.ioccontainer.starter.XmlBeanScopeIocContainer
1.5.5 自定义作用域
bean
作用域机制是可扩展的。你可以定义你自己的作用域或者甚至重定义存在的作用域,尽管后者被认为是不好的做法,你不能覆盖内置的单例和原型范围。
-
创建一个自定义作用域
去集成你的自定义作用域到
Spring
容器中,你需要去实现org.springframework.beans.factory.config.Scope
接口,在这章中描述。有关如何实现自己的作用域的想法,查看Scope
实现提供关于Spring
框架自身和Scope
的文档,其中详细说明了你需要实现的方法。Scope
接口有四个方法从作用域获取对象,从作用域移除它们,并且让它们销毁。例如:
Sesson
的scope
实现返回Season
作用域bean
(如果它不存在,这个方法返回一个新的bean
实例,将其绑定到会话以供将来引用)。 下面的方法从底层作用域返回对象:Object get(String name, ObjectFactory<?> objectFactory)
例如:
Session
的scope
实现移除Season作用域bean
从底层的Session
。对象应该被返回,但是如果这个对象指定的名称不存在你也可以返回null
。下面的方法从底层作用域移除对象:Object remove(String name)
以下方法注册在销毁作用域或销毁作用域中的指定对象时应执行的回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
查看javadoc或者
Spring
作用域实现关于更多销毁回调的信息。以下方法获取基础作用域的会话标识符:
String getConversationId()
这个表示每个作用域是不同的。对于
Session
作用域的实现,此标识符可以是Session
标识符。 -
使用自定义作用域
在你写并且测试一个或更多自定义
Scope
实现,你需要去让Spring
容器知道你的新作用域。以下方法是在Spring
容器中注册新范围的主要方法:void registerScope(String scopeName, Scope scope);
这个方法在
ConfigurableBeanFactory
接口上被定义,该接口可通过Spring
附带的大多数具体ApplicationContext
实现上的BeanFactory
属性获得。registerScope(..)
方法第一个参数是唯一的名字关于作用域。Spring
容器本身中的此类名称示例包括单例和原型。registerScope(..)
方法第二个参数是自定义Scope
实现假设你写你的自定义
Scope
实现并且像下面的例子注册它。接下来例子使用
SimpleThreadScope
,它包括Spring
但是默认是不被注册的。对于你自己的自定义范围实现,是相同的。Scope threadScope = new SimpleThreadScope(); //注册自定义作用域 beanFactory.registerScope("thread", threadScope);
然后,你可以按照你的自定义范围的作用域规则创建bean定义,如下所示:
<bean id="..." class="..." scope="thread">
通过自定义
Scope
实现,你不仅限于以编程方式注册作用域。你可以声明式注册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()
返回对象。参考代码:
com.liyong.ioccontainer.starter.XmlCustomScopeIocContainer
作者
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。
博客地址: http://youngitman.tech