天天看点

Spring 5 中文解析核心篇-IoC容器之Bean作用域

当你创建一个

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 5 中文解析核心篇-IoC容器之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

的单例作用域:

Spring 5 中文解析核心篇-IoC容器之Bean作用域

数据访问对象(

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一样,您可以根据需要任意更改所创建实例的内部状态,因为知道其他也在使用从同一

`userPreferences

Bean定义创建的实例的HTTP Session实例也看不到这些状态变化,因为它们特定于单个HTTP会话。当

HTTP

会话最终被丢弃时,作用于该特定

HTTP

会话的

bean

也将被丢弃。

Java Config

@SessionScope

session

@SessionScope
@Component
public class UserPreferences {
    // ...
}           
  • Application作用域

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>           
  1. 这行定义代理。

创建一个代理,通过插入一个子

<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

CSDN:

https://blog.csdn.net/liyong1028826685

微信公众号:

Spring 5 中文解析核心篇-IoC容器之Bean作用域

技术交流群:

Spring 5 中文解析核心篇-IoC容器之Bean作用域