掘金 后端 ( ) • 2021-06-08 17:57
.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px}.markdown-body h2{padding-bottom:12px;font-size:24px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

原文地址:docs.spring.io/spring-fram…

1.5 对象的作用域

​当您创建一个bean定义时,您将设置由该bean定义的类创建实际实例的方式。您不仅可以控制要插入到特定bean定义创建的对象中的各种依赖项和配置值,还可以控制特定bean定义创建的对象的作用域。这种方法强大而灵活,因为您可以通过配置选择创建的对象的范围。可以将bean定义为部署在多个作用域中之一。Spring框架支持6个作用域,其中4个只有当你使用web感知的ApplicationContext时才可用。您还可以创建自定义范围。

ScopeDescriptionsingleton(默认)将每个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上下文中有效。

1.5.1 singleton

​只存在一个共享实例的单例bean被容器管理,所有对该bean定义的bean的请求都会导致Spring容器返回该特定的bean实例。

​换句话说,当您定义一个bean定义并将其作用域限定为singleton的时,Spring IoC容器创建该bean定义的对象的一个实例。此单一实例存储在此类单例bean的缓存中,对该命名bean的所有后续请求和引用都返回缓存的对象。下面的图片显示了单例作用域是如何工作的:

image.png

Spring的单例概念不同于GoF中定义的单例模式。GoF单例使用硬编码,使每个加载器只创建一个类的实例。Spring单例对象的作用域描述每个容器和每个bean。这意味着,如果您在单个Spring容器中为特定类定义为singleton,那么Spring容器将创建由该bean定义的类的一个且仅一个实例。单例作用域是Spring的默认作用域。你可以如下所示在XML中定义一个单例bean:

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

<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
复制代码

1.5.2 Prototype

bean的非单例作用域导致的结果是每次对特定bean发出请求时都创建一个新的bean实例。也就是说,该bean被注入到另一个bean中,或者通过getBean()方法调用请求它都会返回一个新构造的对象。存在以下规则:您应该对所有有状态的对象使用prototype作用域,而对无状态对象使用singleton作用域。

下图说明了Spring的prototype作用域:

image.png

数据访问对象(DAO)通常不被配置为prototype,因为典型的DAO不保存任何状态,使用singleton会更加合适。

下面的例子用XML将bean定义为prototype:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
复制代码

​与其他作用域相比,Spring不管理prototype bean的完整生命周期。容器实例化、配置和组装一个prototype对象,并将其交给客户端后,不再有该prototype实例的进一步记录。在任意作用域下,容器都会调用对象的初始化生命周期回调方法,但在prototype下,不会调用配置的销毁生命周期回调方法;客户端代码必须清理prototype作用域的对象,并释放持有的昂贵资源。要让Spring容器释放由prototype作用域bean持有的资源,请尝试使用对需要清理的bean的引用的自定义bean后处理程序。

在某些方面,Spring容器相对于prototype作用域的角色是Java new操作符的替代。对象创建后的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参见生命周期回调)

1.5.3 带有Prototype依赖关系的Singleton bean

​当您使用依赖于Prototype bean的Singleton bean时,请注意依赖项是在实例化时解析的。因此,如果您将一个Prototype作用域的bean注入到Singleton作用域的bean中,那么将实例化一个新的bean,然后将依赖注入到单例bean中。Prototype实例是提供Singleton bean的唯一实例。

​然而,假设您希望Singleton bean在运行时反复获得Prototype bean的一个新实例。不能将Prototype bean依赖注入到单例bean中,因为这种注入只在Spring容器实例化单例bean解析并注入它的依赖项时发生一次。如果你在运行时不止一次需要Prototype bean的新实例,请参阅方法注入

1.5.4 Request, Session, Application,和WebSocket

request、session、application和websocket作用域只有在你使用web的Spring ApplicationContext实现(比如XmlWebApplicationContext)时才可用。如果您将这些作用域与常规的Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,将抛出一个IllegalStateException异常。

初始化Web配置

为了支持request、session、application和websocket级别(web作用域)作用域,在定义bean之前需要进行一些次要的初始配置。(标准作用域:singleton和prototype不需要这个初始设置) 如何完成这个初始设置取决于特定的Servlet环境。

  • 如果您在Spring Web MVC中访问作用域bean,实际上是在Spring DispatcherServlet处理的请求中访问,那么不需要任何特殊设置。DispatcherServlet已经完成了相关配置。

  • 如果你使用Servlet 2.5 web容器,在Spring的DispatcherServlet之外处理请求(例如,当使用JSF或Struts时),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。对于Servlet 3.0+,这可以通过使用WebApplicationInitializer接口以编程方式完成。或者,对于较旧的容器,在web应用程序的web.xml文件中添加以下声明:


    ...
    
        
            org.springframework.web.context.request.RequestContextListener
        
    
    ...

复制代码

另外,如果您的监听器设置有问题,可以考虑使用Spring的RequestContextFilter。过滤器映射取决于web应用程序配置,因此您必须适当地更改它。下面的清单显示了一个web应用程序的过滤器部分配置:


    ...
    
        requestContextFilter
        org.springframework.web.filter.RequestContextFilter
    
    
        requestContextFilter
        /*
    
    ...

复制代码

DispatcherServlet、RequestContextListener和RequestContextFilter都做一件同样的事情:即将HTTP请求对象绑定到服务该请求的线程。这使得请求作用域和会话作用域的bean在调用链的更下游可用。

Request作用域

XML配置如下:


复制代码

Spring容器通过为每个HTTP请求使用LoginAction bean定义来创建LoginAction bean的新实例。也就是说,loginAction bean的作用域在HTTP请求级别。您可以随心所欲地更改已创建实例的内部状态,因为从相同的loginAction bean定义创建的其他实例不会看到这些状态更改。当请求完成处理后,将丢弃作用域为Request的bean。

当使用注释驱动组件或Java配置时,可以使用@RequestScope注释为类配置范围:

@RequestScope
@Component
public class LoginAction {
    // ...
}
复制代码

Session作用域

XML配置如下:


复制代码

Spring容器通过在单个HTTP Session的生命周期中使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean有效地限定在HTTP Session级别。与Request作用域一样,你可以改变内部状态的实例,其他通过userPreferences bean定义创建的HTTP Session对象看不到这些变化状态,因为他们是特定于一个单独的HTTP会话的。当最终丢弃HTTP会话时,作用域为该特定HTTP Session的bean也将被丢弃。

在使用注释驱动组件或Java配置时,可以使用@SessionScope注释为类配置范围:

@SessionScope
@Component
public class UserPreferences {
    // ...
}
复制代码

Application作用域

XML配置如下:


复制代码

Spring容器对整个web应用程序使用AppPreferences bean定义来创建一个AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在ServletContext级别,并存储常规ServletContext属性。这有点类似于Singleton,但在两个重要方面不同:它是单例是基于ServletContext不是Spring ApplicationContext(可能包含几个ServletContext)的。

在使用注释驱动组件或Java配置时,可以使用@ApplicationScope注释为类配置范围:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
复制代码

作用域对象作为依赖项

Spring IoC容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的连接。如果您想将(例如)一个HTTP请求作用域的bean注入到另一个长期作用域的bean中,您可以选择注入一个AOP代理来代替这个作用域bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(如HTTP请求)检索实际目标对象,并对实际对象进行委托方法调用。

以下示例中的配置只有一行,但是理解其中的“为什么”和“如何做”非常重要:




    
    
        
         
    

    
    
        
        
    

复制代码

要创建这样的代理,需要将aop:scoped-proxy/元素插入到一个scoped bean定义中(请参阅选择要创建的代理类型和基于XML模式的配置)。为什么在请求、会话和定制作用域级别定义bean需要aop:scoped-proxy/元素呢?考虑以下单例bean定义,并将其与您需要为上述范围定义的内容进行对比(注意以下userPreferences bean定义是不完整的):




    

复制代码

​在前面的示例中,单例bean (userManager)被注入一个HTTP Session作用域 (userPreferences)的引用。这里突出的一点是userManager bean是一个单例对象:每个容器只实例化它一次,并且它的依赖项(在本例中只有一个,即userPreferences bean)也只被注入一次。这意味着userManager bean只对完全相同的userPreferences对象(即最初注入它的对象)进行操作。

​当您将一个较短作用域的bean注入到较长作用域的bean中时,这不是您想要的行为(例如,将一个HTTP会话作用域的协作bean作为依赖项注入到单例bean中)。相反,您需要一个userManager对象,并且在HTTP会话的生命周期内,您需要一个特定于HTTP会话的userPreferences对象。因此,容器创建一个对象,该对象公开与UserPreferences类完全相同的公共接口(理想情况下,该对象是UserPreferences实例),它可以从作用域机制(HTTP请求、会话等)获取真正的UserPreferences对象。容器将这个代理对象注入到userManager bean中,该bean并不知道这个UserPreferences引用是一个代理。在本例中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,它实际上是在调用代理上的方法。然后代理从HTTP Session(在本例中)获取真实的UserPreferences对象,并将方法调用委托给检索到的真实UserPreferences对象。

​因此,在将请求和会话范围的bean注入协作对象时,您需要以下(正确和完整的)配置,如下面的示例所示:


    



    

复制代码

选择代理类型

默认情况下,当Spring容器为用元素标记的bean创建代理时,将创建一个基于cglib的类代理。

CGLIB代理只拦截公共方法调用!不要在这样的代理上调用非公共方法。它们没有被委托给实际的作用域目标对象。

或者,通过为aop:scoped-proxy/元素的proxy-target-class属性的值指定false,您可以配置Spring容器为这种作用域bean创建基于JDK接口的标准代理。使用基于JDK接口的代理意味着您不需要在应用程序类路径中添加其他库来实现这种代理。然而,这也意味着作用域bean的类必须实现至少一个接口,所有被注入该作用域bean的协作者必须通过它的一个接口引用该bean。下面的例子显示了一个基于接口的代理:



    



    

复制代码

有关选择基于类的代理或基于接口的代理的详细信息,请参考:docs.spring.io/spring-fram…

1.5.5 自定义作用域

创建自定义作用域

要将您的自定义作用域集成到Spring容器中,您需要实现将在本节中描述的org.springframework.beans.factory.config.Scope接口。要了解如何实现自定义作用域,请参阅Spring框架提供的几种Scope实现和Scope javadoc docs.spring.io/spring-fram…

Scope接口有四个方法,用于从作用域获取对象、从作用域移除对象并销毁它们。

例如,会话作用域实现返回会话作用域bean(如果它不存在,该方法将bean绑定到会话以备将来引用后返回该bean的新实例)。下面的方法从底层作用域返回对象:

Object get(String name, ObjectFactory> objectFactory)
复制代码

例如,session作用域实现从底层会话中删除会话作用域bean。这个方法会返回该对象,但如果没有找到指定名称的对象,则返回null。以下方法将对象从基础作用域中移除:

Object remove(String name)
复制代码

以下方法注册了一个回调函数,当该scope被销毁时,或者当该范围中的指定对象被销毁时,该回调函数应该被调用:

void registerDestructionCallback(String name, Runnable destructionCallback)
复制代码

有关销毁回调的更多信息,请参见javadoc或Spring作用域实现。

以下方法获取底层作用域的会话标识符:

String getConversationId()
复制代码

这个标识符对于每个作用域都是不同的。对于会话范围的实现,此标识符可以是会话标识符。

使用自定义作用域

在编写和测试一个或多个自定义Scope实现之后,您需要让Spring容器知道您的新作用域。下面的方法是向Spring容器注册一个新的Scope的方法:

void registerScope(String scopeName, Scope scope);
复制代码

此方法在ConfigurableBeanFactory接口上声明

第一个参数是与作用域关联的唯一名称。Spring容器本身中的此类名称的例子有singleton和prototype。

第二个参数是您希望注册和使用的自定义Scope实现的实际实例。

假设您编写了自定义Scope实现,然后如下一个示例所示注册它。

下一个示例使用SimpleThreadScope,它包含在Spring中,但默认情况下没有注册。对于您自己的自定义Scope实现,说明将是相同的。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope)
复制代码

然后,您可以创建符合自定义Scope范围规则的bean定义,如下所示:


复制代码

有了自定义的Scope实现,您就不限于该范围的编程注册。您也可以通过使用CustomScopeConfigurer类以声明的方式进行范围注册,如下面的示例所示:




    
        
            
                
                    
                
            
        
    

    
        
        
    

    
        
    


复制代码