Bean的作用域
创建 bean 定义时,将创建一个模板来创建该 bean 定义所定义的类实例。模板这个概念很重要,因为它意味着我们可以从一个模板中创建许多对象实例。
我们不仅可以控制要插入到特定 bean 定义创建的对象中的各种依赖和配置值,还可以控制特定 bean 定义创建的对象的作用域,我们可以配置bean实例的作用域。 Spring 框架支持六个作用域,其中有四个只有web应用的ApplicationContext
才可用。我们也可以自定义bean的作用域。
Bean的六大作用域
Scope | Description |
---|---|
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 中有效。 |
单例作用域
仅管理一个 singleton bean 的一个共享实例,并且所有具有 ID的bean如果与这个单例beanID相匹配时,这次bean请求都会导致该特定的单例 bean 实例由 Spring 容器返回。
换句话说,当定义一个 bean 定义并且其作用域为单例时,Spring IoC 容器将为该 bean 定义所定义的对象创建一个实例。该单实例存储在此类单例 bean 的高速缓存中,并且对该bean的所有后续请求和引用都返回该高速缓存中的对象。下图显示了单例作用域如何工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1kN0xO9-1670911296566)(https://www.docs4dev.com/images/spring-framework/5.1.3.RELEASE/singleton.png)]
Spring 的 singleton bean 的概念与“四人帮”(GoF)模式一书中定义的 singleton 模式不同。 GoF 单例对对象的范围进行硬编码,以使每个 ClassLoader 只能创建特定类的一个实例,而Spring的单例的作用域应描述为每个容器的单例bean。 这意味着,如果在单个 Spring 容器中为特定类定义一个单例bean,则 Spring 容器将保证创建该 bean 定义所定义的类的实例只有一个,但是其他容器的该bean定义不能保证是单例的。 Singleton 范围是 Spring 中的默认范围。要将 bean 定义为 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发起请求时,IoC容器部署的原型bean都会创建一个新 bean 实例。即将 Bean 注入到另一个 Bean 中,或者通过容器上的getBean()
方法调用来请求它时都会返回新原型bean实例。通常,应将原型作用域用于所有有状态的Bean,将单例作用域用于无状态的bean。
下图说明了 Spring 原型范围:
以下示例将 bean 的作用域定义为原型作用域:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域相反,Spring 不管理原型 Bean 的完整生命周期。容器将实例化,配置或组装原型对象,然后将其交给 Client 端,而无需对该原型实例的进一步记录。因此,尽管在不考虑作用域的情况下在所有对象上都调用了初始化生命周期回调方法,但在原型的情况下,Spring容器不会对原型bean调用已配置的销毁生命周期回调。 Client 端代码必须清除原型作用域内的对象,并释放原型 Bean 拥有的资源。要使 Spring 容器释放由原型作用域的 bean 占用的资源,需要使用自定义bean的后置处理器,其包含对需要清理的 bean 的引用。
在某些方面,Spring 容器在原型bean 方面的应用可以看做是 Java new
运算符的替代。超过该时间点的所有生命周期的管理都将由 Client 端处理。
具有原型 Bean 依赖关系的单例 Bean
当使用对原型 bean 有依赖的单例作用域 Bean 时,请注意,依赖关系在实例化时已解决。因此,如果将原型 bean依赖注入到单例bean 中时会实例化新的原型 bean,然后再将这个新原型bean依赖注入到单例 bean 中。原型bean实例是曾经提供给单例bean 的唯一实例。
但如果希望单例bean 在运行时重复获取原型bean 的新实例。就不能将原型bean 依赖注入到单例 bean 中,因为当 Spring 容器实例化单例 bean 并解析其依赖然后依赖注入时,该注入仅发生一次。如果在单例bean运行时多次需要一个原型 bean 的新实例,就不能使用依赖注入了,而要使用之前我们学的方法注入。
请求,会话,应用程序和 WebSocket作用域
request
,session
,application
和websocket
作用域仅在使用 Web项目的 Spring ApplicationContext
实现(如XmlWebApplicationContext
)时可用。如果将这些作用域与常规的 Spring IoC 容器(例如ClassPathXmlApplicationContext
)一起使用,则会引发未知 bean 作用域的IllegalStateException
异常。
初始 Web 配置
为了支持request
,session
,application
和websocket
级别(网络作用域的 Bean)的 Bean作用域,在定义 Bean 之前,需要一些较小的初始配置。 (对于标准范围singleton
和prototype
,不需要此初始设置.)
如何完成此初始设置取决于特定的 Servlet 环境。
如果实际上在 Spring DispatcherServlet
处理的请求中访问 SpringMVC 中的作用域 Bean,则不需要特殊的设置。因为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
。过滤器 Map 取决于周围的 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 可以在调用链的更下游使用。
Request scope
考虑以下 XML 配置来定义 bean:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器通过为每个 HTTP 请求使用loginAction
bean 定义来创建LoginAction
bean 的新实例。也就是说,loginAction
bean 的作用域是 HTTP 请求级别。我们可以根据需要更改所创建实例的内部状态,因为从同一loginAction
bean 定义创建的其他实例看不到这些状态更改。它们特定于单个请求。当请求完成处理时,将限制作用于该HTTP请求的 Bean。
使用注解驱动的组件或 Java 配置时,可以使用@RequestScope
注解将组件分配给request
作用域。以下示例显示了如何执行此操作:
@RequestScope
@Component
public class LoginAction {
// ...
}
Session Scope
考虑以下 XML 配置来定义 bean:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
在单个 HTTP Session
的生命周期内,Spring 容器通过使用userPreferences
bean 定义来创建UserPreferences
bean 的新实例。即userPreferences
bean 的作用域就是 HTTP Session
的作用域。与request作用域的 Bean 一样,我们可以根据需要任意更改所创建实例的内部状态,因为其他 HTTP Session
实例(也使用从相同userPreferences
Bean 定义创建的实例)不会看到这些状态更改,因为它们特定于单个 HTTP Session
。当最终丢弃 HTTP Session
时,也将丢弃作用于该特定 HTTP Session
的 bean。
使用注解驱动的组件或 Java 配置时,可以使用@SessionScope
注解 将组件分配给session
范围。
@SessionScope
@Component
public class UserPreferences {
// ...
}
Application Scope
使用以下 XML 配置来定义应用上下文作用域的bean:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器通过对整个 Web 应用程序使用appPreferences
bean 定义来创建AppPreferences
bean 的新实例。也就是说,appPreferences
bean 的作用域为ServletContext
级别,并存储为常规ServletContext
属性。这有点类似于 Spring 单例 bean,但是有两个重要的区别:它是每个ServletContext
而不是每个SpringApplicationContext
(在任何给定的 Web 应用程序中可能都有多个),并且实际上是公开的,因此可见为ServletContext
属性。
使用注解驱动的组件或 Java 配置时,可以使用@ApplicationScope
注解 将组件分配给application
范围。以下示例显示了如何执行此操作:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
小总结:
其实我们可以把这3个web作用域都看做特殊的单例作用域,只是对不同级别的单例,例如request作用域是对每一个HTTP请求来说某个bean是单例的。也可以这么理解,这3个web都是有作用范围的原型作用域,原型作用域的bean只要请求就会返回一个新的bean,但这3个请求时只有超出了其作用范围时才会返回新bean。
另外关于WebSocket作用域,其与WebSocket会话的生命周期相关联,这里我们不过多介绍。
有作用域的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()
调用在需要时每次按需检索当前实例无需保留该实例或对其进行存储分别。
关于此节用的不多,不过多介绍以防增加学习压力。
自定义作用域
Bean 作用域机制是可扩展的,即我们可以定义自己的作用域,甚至 重新定义现有作用域(但不建议),并且不能覆盖内置的singleton
和prototype
作用域。
创建自定义作用域
要将自定义作用域集成到 Spring 容器中,首先需要实现org.springframework.beans.factory.config.Scope
接口。
Scope
接口有四种方法可以从作用域中获取对象,从范围中删除对象,然后销毁它们。
例如,会话作用域实现返回会话作用域的 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 容器中注册新的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">
使用自定义Scope
实现,不仅可以通过程序注册该范围。还可以通过使用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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://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>
当您将<aop:scoped-proxy/>
放在FactoryBean
实现中时,作用域是工厂 bean 本身,而不是getObject()
返回的对象。
感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。