Spring官方文档解读(四)之Bean的作用域

Bean的作用域

创建 bean 定义时,将创建一个模板来创建该 bean 定义所定义的类实例。模板这个概念很重要,因为它意味着我们可以从一个模板中创建许多对象实例。

我们不仅可以控制要插入到特定 bean 定义创建的对象中的各种依赖和配置值,还可以控制特定 bean 定义创建的对象的作用域,我们可以配置bean实例的作用域。 Spring 框架支持六个作用域,其中有四个只有web应用的ApplicationContext才可用。我们也可以自定义bean的作用域。

Bean的六大作用域

ScopeDescription
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 原型范围:

prototype

以下示例将 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作用域

requestsessionapplicationwebsocket作用域仅在使用 Web项目的 Spring ApplicationContext实现(如XmlWebApplicationContext)时可用。如果将这些作用域与常规的 Spring IoC 容器(例如ClassPathXmlApplicationContext)一起使用,则会引发未知 bean 作用域的IllegalStateException异常。

初始 Web 配置

为了支持requestsessionapplicationwebsocket级别(网络作用域的 Bean)的 Bean作用域,在定义 Bean 之前,需要一些较小的初始配置。 (对于标准范围singletonprototype,不需要此初始设置.)

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

如果实际上在 Spring DispatcherServlet处理的请求中访问 SpringMVC 中的作用域 Bean,则不需要特殊的设置。因为DispatcherServlet已经公开了所有相关状态。

如果使用 Servlet 2.5 Web 容器,并且在 Spring 的DispatcherServlet之外处理请求(例如使用 JSF 或 Struts 时),则需要注册org.springframework.web.context.request.RequestContextListenerServletRequestListener。对于 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>

DispatcherServletRequestContextListenerRequestContextFilter都做完全相同的事情,即将 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 作用域机制是可扩展的,即我们可以定义自己的作用域,甚至 重新定义现有作用域(但不建议),并且不能覆盖内置的singletonprototype作用域。

创建自定义作用域

要将自定义作用域集成到 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 容器本身中的此类名称示例为singletonprototyperegisterScope(..)方法的第二个参数是您希望注册和使用的自定义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()返回的对象。


感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值