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

当你建立一个bean的定义时候,你能够建立一个模版(recipe)经过bean定义的类定义去建立一个真实的实例。bean定义是模版(recipe)的概念很重要,由于这意味着,与使用类同样,你能够从一个模版(recipe)建立多个对象实例。html

你不只能够控制要插入到从特定bean定义建立的对象中的各类依赖项和配置值,还能够控制从特定bean定义建立的对象的做用域。这种方法是很是有用的和灵活的,由于你能够选择经过配置建立的对象的做用域,而没必要在Java类级别上考虑对象的做用域。bean可以定义部署到一个或多个做用域。Spring框架支撑6种做用域,4种仅仅使用web环境。你能够建立定制的做用域。java

下面的表格描述了支撑的做用域:git

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。更多关于怎样去注册和自定义做用域,查看自定义做用域web

1.5.1 单例bean做用域

单例bean仅仅只有一个共享实例被容器管理,而且全部对具备与该bean定义相匹配的IDbean的请求都会致使该特定bean实例被Spring容器返回。换一种方式,当你定义一个bean的定义而且它的做用域是单例的时候,Spring IoC容器建立经过bean定义的对象定义的实例。这个单例存储在缓存中,而且对命名bean的全部请求和引用返回的是缓存对象。下面图片展现了单例bean做用域是怎样工做的:spring

singleton

Spring的单例bean概念与在GoF设计模式书中的单例模式不一样。GoF单例硬编码对应的做用域例如:只有一个特定类的对象实例对每个ClassLoader只建立一个对象实例。最好将Spring单例的范围描述为每一个容器和每一个bean(备注:GoF设计模式中的单例bean是针对不一样ClassLoader来讲的,而Spring的单例是针对不一样容器级别的)。这意味着,若是在单个Spring容器对指定类定义一个beanSpring容器经过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的单例做用域:api

prototype

数据访问对象(DAO)一般不被配置做为一个原型,由于典型的DAO不会维持任何会话状态。咱们能够更容易地重用单例图的核心。缓存

下面例子在XML中定义一个原型bean安全

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

与其余做用域对比,Spring没有管理原型bean的完整生命周期。容器将实例化、配置或以其余方式组装原型对象,而后将其交给客户端,无需对该原型实例的进一步记录。所以,尽管初始化生命周期回调函数在全部对象上被回调而无论做用域如何,在原型状况下,配置销毁生命周期回调是不被回调。客户端代码必须清除原型做用域内的对象并释放原型Bean占用的昂贵资源。为了让Spring容器释放原型做用域bean所拥有的资源,请尝试使用自定义beanpost-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

requestsessionapplication、和websocket做用域仅仅在你使用SpringApplicationContext实现(例如:XmlWebApplicationContext)时有效。若是你将这些做用域与常规的Spring IoC容器(例如ClassPathXmlApplicationContext)一块儿使用,则会抛出一个IllegalStateException异常,该错抛出未知的bean做用域。

  • 初始化Web配置

    为了支持这些bean的做用域在requestsessionapplication、和websocket级别(web做用域bean)。一些次要的初始化配置在你定义你的bean以前是须要的。(这个初始化安装对于标准的做用域是不须要的:singletonprototype)。

    如何完成这个初始化安装依赖于你的特定Servlet环境。

    若是在Spring Web MVC中访问做用域bean,实际上,在由Spring DispatcherServlet处理的请求中,不须要特殊的设置。DispatcherServlet已经暴露了全部相关状态。

    若是你使用Servlet 2.5 Web容器,请求处理在SpringDispatcherServlet外(例如:当使用JSFStructs),你须要去注册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>

    或者,若是你的监听器设置有问题,考虑使用SpringRequestContextFilter。过滤器映射取决于周围的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>

    DispatcherServletRequestContextListener

    RequestContextFilter所作的事情是同样的,即将HTTP请求对象绑定到为该请求提供服务的线程。这使得requestsession范围的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做用域

    考虑下面的XML关于bean的定义:

    <bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

    Spring容器经过使用appPreferences的bean定义为整个Web应用建立一个AppPreferences的bean新实例。也就是说,appPreferences的做用域在ServletContext级别而且做为一个常规的ServletContext属性被储存。这个和Spring的单例bean相似,但有两个重要的区别:每一个ServletContext是一个单例,而不是每一个SpringApplicationContext(在给定的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>,提供了一些附加的获取方式,包括getIfAvailablegetIfUnique

    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的定义在requestsession和自定义做用域须要<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-scopedsession-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接口有四个方法从做用域获取对象,从做用域移除它们,而且让它们销毁。

    例如:Sessonscope实现返回Season做用域bean(若是它不存在,这个方法返回一个新的bean实例,将其绑定到会话以供未来引用)。 下面的方法从底层做用域返回对象:

    Object get(String name, ObjectFactory<?> objectFactory)

    例如:Sessionscope实现移除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/liyong1...

微信公众号:

技术交流群:

相关文章
相关标签/搜索