手记

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定义相匹配的IDbean的请求都会导致该特定bean实例被Spring容器返回。换一种方式,当你定义一个bean的定义并且它的作用域是单例的时候,Spring IoC容器创建通过bean定义的对象定义的实例。这个单例存储在缓存中,并且对命名bean的所有请求和引用返回的是缓存对象。下面图片展示了单例bean作用域是怎样工作的:

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的单例作用域:

数据访问对象(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一样,您可以根据需要任意更改所创建实例的内部状态,因为知道其他也在使用从同一``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是一个单例,而不是每个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

0人推荐
随时随地看视频
慕课网APP