当您创建一个 bean 定义时,您创建了一个配方,用于创建由该 bean 定义的类的实际实例。bean 定义是一个配方的想法很重要,因为这意味着,与类一样,可以从一个配方创建多个对象实例。
您不仅可以控制要插入从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围。这种方法功能强大且灵活,因为您可以通过配置选择创建的对象的作用域,而不必在 Java 类级别上嵌入对象的作用域。可以将 bean 定义为部署在一系列作用域中的一个。Spring 框架支持 6 个作用域,其中 4 个只有当你使用 web 感知的 ApplicationContext 时才可用。您还可以创建自定义作用域。
singleton (单例) | (默认)将单个 bean 定义限定为每个 Spring IoC 容器的单个对象实例。 |
---|---|
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 上下文中有效。 |
:::tips 从 Spring 3.0 开始,线程范围可用,但默认情况下未注册。有关详细信息,请参阅 SimpleThreadScope. 有关如何注册此或任何其他自定义范围的说明,请参阅 使用自定义范围。 :::
单例 singleton 作用域
只有一个单例 bean 的共享实例被管理,并且所有对具有与该 bean 定义匹配的一个或多个 ID 的 bean 的请求都会导致 Spring 容器返回一个特定的 bean 实例。
简单说:一个 bean 被定义为单例时,Spring Ioc 容器只会创建该 bean 定义的一个实例,这个实例会被存储在缓存中。下图显示了单例作用域的工作原理:
Spring 的单例 bean 概念不同于 GoF 模式书中定义的单例模式。GoF 单例对对象的范围进行硬编码,以便每个 ClassLoader 创建一个且仅一个特定类的实例。Spring 单例的范围最好描述为 每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会创建该 bean 定义定义的类的一个且仅一个实例。单例范围是 Spring 中的默认范围。要将 bean 定义为 XML 中的单例,您可以定义一个 bean,如下例所示:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 以下是等效的,尽管是冗余的(默认情况下是单例范围) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
多例 prototype 作用域
每次对多例的 bean 发出请求时,都会创建一个新的 bean 实例。也就是说,将 bean 注入到另一个 bean 中,或者您通过容器上的 getBean()
方法调用来请求它。通常,您应该对所有 有状态 bean 使用 prototype 范围,对无状态 bean 使用单例范围。
如上图所示:每次注入到新的 bean 中,都会创建一个新的实例注入。
(数据访问对象 (DAO) 通常不配置为 prototype,因为 DAO 不保存任何会话状态。
下面是将一个 bean 配置为多例的例子
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他范围相比,Spring 不管理多例 bean 的完整生命周期。容器实例化、配置和以其他方式组装一个多例对象,并将其交给客户机,而不再记录该多例实例。因此,尽管对所有对象调用了初始化生命周期回调方法,而不考虑范围,但在多例的情况下,不会调用配置的销毁生命周期回调。客户机代码必须清理多例范围内的对象,并释放多例 bean 所拥有的昂贵资源。为了让 Spring 容器释放多例范围内的 bean 所拥有的资源,可以尝试使用一个定制的 bean 后处理器,它包含对需要清理的 bean 的引用。
在某些方面,Spring 容器在多例范围 bean 中的角色是 Java new 操作符的替代品。所有超过该点的生命周期管理都必须由客户处理。(有关 Spring 容器中 bean 生命周期的详细信息,请参阅 生命周期回调。)
将多例 bean 作为依赖注入到单例 bean
多例 bean 每次注入到单例 bean 中的时候,都会新创建一个实例注入。
但是,如果你希望单例 bean 的某些操作中,需要在运行时重复获取多例 bean 的新的实例,就需要前面讲到过的 方法注入 了,而不能直接当成依赖项注入,因为这只会发生一次注入时机。
request、session、application、websocket 作用域
只有当你使用一个 web 的 Spring ApplicationContext 实现(比如 XmlWebApplicationContext)时,request、session、application 和 websocket 作用域才可用。如果将这些作用域与常规的Spring IoC容器(如 ClassPathXmlApplicationContext)一起使用,就会抛出一个 IllegalStateException,该 exception 会报错未知的 bean 作用域。
初始 web 配置
要使用这 4 种作用域,在定义 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 文件中:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
或者,如果您的侦听器设置存在问题,请考虑使用 Spring 的 RequestContextFilter. 过滤器映射取决于周围的 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>
DispatcherServlet、RequestContextListener 和 RequestContextFilter 都做完全相同的事情,即将 HTTP 请求对象绑定到服务该请求的线程。 这使得具有请求和会话作用域的 bean 可以在调用链的后面使用。
request 作用域
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器通过为每个 HTTP 请求使用 LoginAction bean 定义来创建 LoginAction bean 的新实例。 也就是说,loginAction bean 的作用域为 HTTP 请求级别。 您可以随心所欲地更改创建的实例的内部状态,因为从相同的 loginAction bean 定义创建的其他实例不会在状态中看到这些更改。 它们是针对个人要求的。 当请求完成处理时,将范围限定在请求的 bean 丢弃。
在使用注解驱动的组件或 Java 配置时,可以使用 @RequestScope
注解将组件分配给 request 作用域。 下面的例子展示了如何做到这一点:
@RequestScope
@Component
public class LoginAction {
// ...
}
session 作用域
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
和 request 类似,在一个 session 生命周期中会实例化一个 bean 的实例。session 销毁时该实例也被销毁。
注解使用如下
@SessionScope
@Component
public class UserPreferences {
// ...
}
application 作用域
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器通过对整个 web 应用程序使用一次 AppPreferences bean 定义来创建一个 AppPreferences bean 的新实例。也就是说,appPreferences bean 的作用域在 ServletContext 级别,并存储为一个常规ServletContext 属性。这有点类似于 Spring 单例 bean,但在两个重要方面有所不同:它是每个ServletContext 的单例,而不是每个 Spring ApplicationContext(在任何给定的 web 应用程序中可能有多个),它实际上是公开的,因此作为 ServletContext 属性可见。
使用注解如下
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关联,适用于 STOMP over WebSocket 应用程序,请参阅 WebSocket 范围 了解更多详细信息。
将作用域的 Bean 作为依赖项
Spring IoC 容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的连接。如果你想(例如)将一个 HTTP 请求作用域的 bean 注入到另一个具有更长生命周期的 bean 中,你可以选择 注入一个 AOP 代理来代替这个作用域 bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)检索真实目标对象,并将方法调用委托给真实对象。
:::info
您还可以在限定为单例的 bean 之间使用 <aop:scoped-proxy/>
,然后引用通过可序列化的中间代理,因此能够在反序列化时重新获取目标单例 bean。
当针对 多例的 bean 声明 <aop:scoped-proxy/>
时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。
此外,作用域代理并不是以生命周期安全的方式从较短的作用域访问 bean 的唯一方法。您还可以将您的注入点(即构造函数或 setter 参数或自动装配字段)声明为 ObjectFactory<MyTargetBean>
,从而允许 getObject()
调用在每次需要时按需检索当前实例 — 无需坚持实例或单独存储它。
作为扩展变体,您可以声明 ObjectProvider<MyTargetBean>
,它提供了几个额外的访问变体,包括 getIfAvailable
和 getIfUnique
。
其 JSR-330 变体称为 Provider,并与 Provider<MyTargetBean>
声明和每次检索尝试的相应 get()
调用一起使用。有关整体 JSR-330 的更多详细信息,请参见 此处。
:::
简单说。要实现一个 在 controller 中注入一个 session 作用域的实例,每次不同的 session 的请求从这个变量中获取数据都是它自己对应 session 的数据,下面这个例子使用我们常见的注解方式来测试
package cn.mrcode.study.springdocsread.web;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
/**
* @author mrcode
* @date 2022/2/11 14:58
*/
@Component
@SessionScope
public class UserPreferences {
private String name;
}
package cn.mrcode.study.springdocsread.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author mrcode
* @date 2022/2/11 15:06
*/
@RestController
@RequestMapping("/test")
public class UserController {
@Autowired
private UserPreferences userPreferences;
@GetMapping("/test")
public String test() {
return userPreferences.toString();
}
}
然后使用不同的访问该地址 [http://localhost:8080/test/test](http://localhost:8080/test/test)
都会看到不同的实例信息。比如下面
cn.mrcode.study.springdocsread.web.UserPreferences@331cada4
cn.mrcode.study.springdocsread.web.UserPreferences@766756ab
相同的会话访问,会返回同一个实例信息。
以下示例中的配置只有一行,但重要的是要了解其背后的 「为什么」以及「如何」:
在 xml 中需要使用 <aop:scoped-proxy/>
来启用
<?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 会话作用域 bean -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!--指示容器代理的 bean -->
<aop:scoped-proxy/>
</bean>
<!-- 一个单例的 bean 需要注入 session 作用域的 bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- 对代理 userPreferences bean 的引用 -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
为什么需要 <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 会话作用域 Bean(UserPreferences)的引用。这里的突出点是 Usermanager Bean 是单例:它是每个容器实例化一次,其依赖关系(在这种情况下只有一个,userPreferences bean)也仅注入一次。这意味着 usermanager bean 仅在完全相同的userPreferences 对象上运行(即,它最初注入的用户)。
当将更短生命周期作用域的 bean 注入更长寿命的作用域 bean 时,这不是您想要的行为(例如,将 HTTP会话作用域的协作 bean 注入为单例 bean)。相反,您需要一个单个 usermanager 对象,并且对于 http 会话的生命周期,您需要一个特定于 HTTP 会话的 UserPreferences 对象。因此,容器创建一个对象,该对象将完全相同的公共接口暴露为 userPreferences 类(理想情况下是一个 userPreferences 实例的对象),它可以从作用域机制(HTTP 请求,会话等)获取真实 userPreferences 对象。该容器将此代理对象注入 UserManager Bean,这不明显,此 UserPreferences 参考是代理。在此示例中,当 UserManager 实例调用依赖于依赖性 userPreferences 对象上的方法时,它实际上正在调用代理上的方法。然后,代理从(在这种情况下)HTTP 会话中获取真实 userPreferences 对象,并将方法调用委托到检索到的真实 UserPreferences 对象上。
:::tips
这里有一个疑问是:单例的 controller 上如果存在并发访问的时候,单例上的成员变量会不会出问题?
从上面那个 controller 测试来看,成员变量直接变成了自己对应 session 作用域的实例。
但是从上面的描述来看,这个成员变量其实是一个固定的代理类,但是单例类里面会委托对应的 session 作用域的实例对象去执行该实例上的方法等功能 :::
选择要创建的代理类型
默认情况下,当 Spring 容器为 <aop:scoped-proxy />
元素标记的 bean 创建代理时,创建基于CGLIB 的类代理。
:::tips
CGLIB 代理拦截仅公共方法调用!不要在这样的代理上调用非公开方法。它们不会委托到实际的作用域目标对象。
:::
或者,您可以配置 Spring 容器,通过为 <aop:scoped proxy/>
元素的 proxy-target-class
属性的值指定 false,为此类作用域 bean 创建基于标准 JDK 接口的代理。使用基于 JDK 接口的代理意味着在应用程序类路径中不需要额外的库来影响这种代理。然而,这也意味着作用域 bean 的类必须实现至少一个接口,并且所有注入作用域 bean 的协作者必须通过其接口之一引用 bean。以下示例显示了基于接口的代理:
<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>
有关选择基于类或基于接口的代理的更详细信息,请参阅 代理机制。
自定义作用域
bean 作用域机制是可扩展的。您可以定义自己的作用域甚至重新定义现有作用域,尽管后者被认为是不良的实践,并且您无法覆盖内置单例和多例。
创建自定义作用域
要将自定义作用域集成到 Spring 容器中,您需要实现 org.springframework.beans.factory.config.scope
接口,它在本节中描述。为了了解如何实现自己的作用域,请参阅 Spring Framework 本身的 Scope Javadoc 提供的作用域实现,这解释了更详细的方法信息。
public interface Scope {
/**
* 根据名称获取对应作用域的实例,如果不存在的话,就调用工厂创建他
* @param name bean 名称
* @param objectFactory 底层创建该对象实例的对象工厂
* @return 返回创建好的实例,一定有返回值,从不会返回 null
* @throws IllegalStateException if the underlying scope is not currently active
*/
Object get(String name, ObjectFactory<?> objectFactory);
/**
从基础范围中删除具有给定name的对象。
如果没有找到对象,则返回null ;否则返回删除的Object 。
请注意,实现还应删除指定对象的已注册销毁回调(如果有)。但是,在这种情况下,它不需要执行注册的销毁回调,因为对象将被调用者销毁(如果合适的话)。
注意:这是一个可选操作。如果实现不支持显式删除对象,则可能会抛出 UnsupportedOperationException 。
*/
@Nullable
Object remove(String name);
/**
以下方法注册一个回调,即在销毁时应调用范围或当范围中的指定对象被销毁时:
*/
void registerDestructionCallback(String name, Runnable callback);
@Nullable
Object resolveContextualObject(String key);
/**
以下方法获取底层范围的对话标识符:
*/
@Nullable
String getConversationId();
}
使用自定义作用域
要注册到 Spring Ioc 容器中,可以使用下面的方法
void registerScope(String scopeName, Scope scope);
此方法在 ConfigurableBeanFactory 接口上声明,该接口可通过 Spring 附带的大多数具体ApplicationContext 实现的 BeanFactory 属性获得。比如下面这样注册
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
context.getBeanFactory().registerScope(name,scope);
有一个 Spring 提供的 SimpleThreadReadScope 对象,但是默认情况下没有注册,如果要使用它,就可以自己注册
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
然后定义一个该作用域的 bean
<bean id="..." class="..." scope="thread">
通过自定义作用域实现,您不仅限于作用域的编程注册。您还可以通过使用 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>
:::tips
当您将