Bean 作用域

当您创建一个 bean 定义时,您实际上是创建了一个用于创建该 bean 定义所定义的类的实际实例的配方(recipe)。 Bean 定义是一个配方的概念很重要,因为这意味着,与类一样,您可以从单个配方创建许多对象实例。

您不仅可以控制要插入到从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围(scope)。 这种方法功能强大且灵活,因为您可以选择通过配置创建的对象的范围,而不必在 Java 类级别烘焙对象的范围。 Bean 可以定义为部署在多种作用域之一中。 TODAY Framework 支持六种作用域,其中四种仅在您使用感知 Web 的 ApplicationContext 时才可用。 您还可以创建 自定义作用域

下表描述了支持的作用域:

Table 1. Bean 作用域
作用域 描述

singleton

(默认) 将单个 bean 定义的作用域限定为每个 Infra IoC 容器一个对象实例。

prototype

将单个 bean 定义的作用域限定为任意数量的对象实例。

request

将单个 bean 定义的作用域限定为单个 HTTP 请求的生命周期。即,每个 HTTP 请求都有自己的 bean 实例,该实例是在单个 bean 定义的后面创建的。仅在感知 Web 的 Infra ApplicationContext 上下文中有效。

session

将单个 bean 定义的作用域限定为 HTTP Session 的生命周期。仅在感知 Web 的 Infra ApplicationContext 上下文中有效。

线程作用域(Thread scope)可用,但默认未注册。有关更多信息,请参阅 SimpleThreadScope 的文档。 有关如何注册此作用域或任何其他自定义作用域的说明,请参阅 使用自定义作用域

单例(Singleton)作用域

只管理单例 bean 的一个共享实例,并且所有对具有与该 bean 定义匹配的 ID 的 bean 的请求都会导致 Infra 容器返回该特定的 bean 实例。

换句话说,当您定义一个 bean 定义并且其作用域为单例时,Infra IoC 容器只创建该 bean 定义所定义的对象的一个实例。 此单个实例存储在此类单例 bean 的缓存中,并且对该命名 bean 的所有后续请求和引用都返回缓存的对象。 下图显示了单例作用域的工作原理:

singleton

Infra 的单例 bean 概念与 Gang of Four (GoF) 模式书中定义的单例模式不同。 GoF 单例硬编码对象的范围,使得每个 ClassLoader 创建且仅创建一个特定类的实例。 Infra 单例的范围最好描述为每个容器和每个 bean。 这意味着,如果您在单个 Infra 容器中为特定类定义一个 bean,则 Infra 容器将创建该 bean 定义所定义的类的一个且仅一个实例。 单例作用域是 Infra 中的默认作用域。要在 XML 中将 bean 定义为单例,您可以像下面示例所示那样定义 bean:

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

<!-- 以下是等效的,尽管是多余的(单例作用域是默认的) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

原型(Prototype)作用域

Bean 部署的非单例原型作用域导致每次请求该特定 bean 时都会创建一个的 bean 实例。 也就是说,bean 被注入到另一个 bean 中,或者您通过容器上的 getBean() 方法调用请求它。 作为规则,您应该对所有有状态 bean 使用原型作用域,对无状态 bean 使用单例作用域。

下图说明了 Infra 原型作用域:

prototype

(数据访问对象 (DAO) 通常不配置为原型,因为典型的 DAO 不持有任何会话状态。我们要重用单例图的核心更容易。)

以下示例在 XML 中将 bean 定义为原型:

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

与其他作用域不同,Infra 不管理原型 bean 的完整生命周期。 容器实例化、配置和以其他方式组装原型对象并将其交给客户端,而不再保留该原型实例的记录。 因此,虽然初始化生命周期回调方法在所有对象上都会调用,而不管作用域如何,但在原型的情况下,不会调用配置的销毁生命周期回调。 客户端代码必须清理原型作用域的对象并释放原型 bean 持有的昂贵资源。 要让 Infra 容器释放原型作用域 bean 持有的资源,请尝试使用自定义 bean 后处理器,它持有对需要清理的 bean 的引用。

在某些方面,Infra 容器在原型作用域 bean 方面的角色是 Java new 运算符的替代品。 此后的所有生命周期管理必须由客户端处理。 (有关 Infra 容器中 bean 生命周期的详细信息,请参阅 生命周期回调。)

具有原型 bean 依赖项的单例 Bean

当您使用具有原型 bean 依赖项的单例作用域 bean 时,请注意依赖项是在实例化时解析的。 因此,如果您将原型作用域的 bean 依赖注入到单例作用域的 bean 中,则会实例化一个新的原型 bean,然后将其依赖注入到单例 bean 中。 该原型实例是唯一提供给单例作用域 bean 的实例。

但是,假设您希望单例作用域的 bean 在运行时重复获取原型作用域 bean 的新实例。 您不能将原型作用域的 bean 依赖注入到您的单例 bean 中,因为该注入只发生一次,即当 Infra 容器实例化单例 bean 并解析和注入其依赖项时。 如果您需要在运行时不止一次地需要原型 bean 的新实例,请参阅 方法注入

Request、Session、Application 和 WebSocket 作用域

requestsessionapplicationwebsocket 作用域仅在您使用感知 Web 的 Infra ApplicationContext 实现(如 XmlWebApplicationContext)时才可用。 如果您将这些作用域与常规 Infra IoC 容器(如 ClassPathXmlApplicationContext)一起使用,则会抛出抱怨未知 bean 作用域的 IllegalStateException

Request 作用域

考虑以下 bean 定义的 XML 配置:

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

Infra 容器通过使用 loginAction bean 定义为每一个 HTTP 请求创建一个新的 LoginAction bean 实例。 也就是说,loginAction bean 的作用域限定为 HTTP 请求级别。 您可以随意更改已创建实例的内部状态,因为从同一 loginAction bean 定义创建的其他实例看不到这些状态更改。 它们是特定于单个请求的。当请求完成处理时,作用域为请求的 bean 将被丢弃。

当使用注解驱动的组件或 Java 配置时,可以使用 @RequestScope 注解将组件分配给 request 作用域。 以下示例显示了如何执行此操作:

  • Java

@RequestScope
@Component
public class LoginAction {
  // ...
}

Session 作用域

考虑以下 bean 定义的 XML 配置:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Infra 容器通过使用 userPreferences bean 定义为单个 HTTP Session 的生命周期创建一个新的 UserPreferences bean 实例。 换句话说,userPreferences bean 有效地限定在 HTTP Session 级别。 与请求作用域的 bean 一样,您可以随意更改已创建实例的内部状态, 知道同样使用从同一 userPreferences bean 定义创建的实例的其他 HTTP Session 实例看不到这些状态更改, 因为它们是特定于单个 HTTP Session 的。 当 HTTP Session 最终被丢弃时,作用域为该特定 HTTP Session 的 bean 也会被丢弃。

当使用注解驱动的组件或 Java 配置时,可以使用 @SessionScope 注解将组件分配给 session 作用域。

  • Java

@SessionScope
@Component
public class UserPreferences {
  // ...
}

作用域 Bean 作为依赖项

Infra IoC 容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的装配。 如果您想将(例如)HTTP 请求作用域的 bean 注入到另一个具有更长寿命作用域的 bean 中,您可能选择注入 AOP 代理来代替作用域 bean。 也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也能够从相关作用域(如 HTTP 请求)中检索真实的目标对象,并将方法调用委托给真实对象。

您也可以在作用域为 singleton 的 bean 之间使用 <aop:scoped-proxy/>, 然后引用通过中间代理,该代理是可序列化的,因此能够在反序列化时重新获取目标单例 bean。

当针对作用域为 prototype 的 bean 声明 <aop:scoped-proxy/> 时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发给该实例。

此外,作用域代理并不是以生命周期安全的方式从较短作用域访问 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">
    <!-- 对代理的 userPreferences bean 的引用 -->
    <property name="userPreferences" ref="userPreferences"/>
  </bean>
</beans>
1 定义代理的行。

要创建这样的代理,您需要将子 <aop:scoped-proxy/> 元素插入到作用域 bean 定义中(参见 选择要创建的代理类型基于 XML Schema 的配置)。

为什么在 requestsession 和自定义作用域级别的 bean 定义需要 <aop:scoped-proxy/> 元素? 考虑以下单例 bean 定义,并将其与您需要为上述作用域定义的内容进行对比(请注意,以下 userPreferences bean 定义是不完整的):

<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 是一个单例:它在每个容器中只实例化一次,其依赖项(在本例中只有一个,即 userPreferences bean)也只注入一次。 这意味着 userManager bean 仅对完全相同的 userPreferences 对象进行操作(即最初注入的那个对象)。

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

因此,当将 request-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>

选择要创建的代理类型

默认情况下,当 Infra 容器为标记有 <aop:scoped-proxy/> 元素的 bean 创建代理时,将创建基于 CGLIB 的类代理。

CGLIB 代理不拦截私有方法。试图在此类代理上调用私有方法将不会委托给实际的作用域目标对象。

或者,您可以配置 Infra 容器为此类作用域 bean 创建基于标准 JDK 接口的代理,方法是将 <aop:scoped-proxy/> 元素的 proxy-target-class 属性的值指定为 false。 使用基于 JDK 接口的代理意味着您不需要在应用程序类路径中使用其他库来实现此类代理。 但是,这也意味着作用域 bean 的类必须实现至少一个接口,并且注入作用域 bean 的所有协作者必须通过其接口之一引用 bean。 以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences 实现了 UserPreferences 接口 -->
<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>

有关选择基于类或基于接口的代理的更多详细信息,请参阅 代理机制

直接注入 Request/Session 引用

作为工厂作用域的替代方案,Infra WebApplicationContext 还支持将 HttpMockRequestHttpMockResponseHttpSessionWebRequest 以及(如果存在 JSF)FacesContextExternalContext 注入到 Infra 管理的 bean 中,只需通过基于类型的自动装配,就像其他 bean 的常规注入点一样。 Infra 通常为此类请求和会话对象注入代理,这具有在单例 bean 和可序列化 bean 中也能工作的优势,类似于工厂作用域 bean 的作用域代理。

自定义作用域

Bean 作用域机制是可扩展的。您可以定义自己的作用域,甚至重新定义现有的作用域,尽管后者被认为是不好的做法,并且您不能覆盖内置的 singletonprototype 作用域。

创建自定义作用域

要将自定义作用域集成到 Infra 容器中,您需要实现 infra.beans.factory.config.Scope 接口,本节将对此进行描述。 要了解如何实现自己的作用域,请参阅 TODAY Framework 本身提供的 Scope 实现以及 Scope javadoc,其中更详细地解释了您需要实现的方法。

Scope 接口有四个方法用于从作用域中获取对象、从作用域中移除对象以及让它们被销毁。

例如,会话作用域实现返回会话作用域的 bean(如果不存在,该方法将返回 bean 的新实例,并将其绑定到会话以供将来参考)。以下方法从底层作用域返回对象:

  • Java

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

例如,会话作用域实现从底层会话中移除会话作用域的 bean。应该返回该对象,但如果未找到具有指定名称的对象,则可以返回 null。以下方法从底层作用域移除对象:

  • Java

Object remove(String name)

以下方法注册一个回调,当作用域被销毁或作用域中的指定对象被销毁时,作用域应调用该回调:

  • Java

void registerDestructionCallback(String name, Runnable destructionCallback)

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

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

  • Java

String getConversationId()

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

使用自定义作用域

在编写并测试一个或多个自定义 Scope 实现后,您需要让 Infra 容器知道您的新作用域。 以下方法是将新 Scope 注册到 Infra 容器的核心方法:

  • Java

void registerScope(String scopeName, Scope scope);

此方法在 ConfigurableBeanFactory 接口上声明,该接口可通过 Infra 附带的大多数具体 ApplicationContext 实现上的 BeanFactory 属性获得。

registerScope(..) 方法的第一个参数是与作用域关联的唯一名称。Infra 容器本身中此类名称的示例包括 singletonprototyperegisterScope(..) 方法的第二个参数是您希望注册和使用的自定义 Scope 实现的实际实例。

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

下一个示例使用 SimpleThreadScope,它包含在 Infra 中,但默认未注册。对于您自己的自定义 Scope 实现,说明是相同的。
  • Java

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

然后,您可以创建遵循自定义 Scope 作用域规则的 bean 定义,如下所示:

<bean id="..." class="..." scope="thread">

对于自定义 Scope 实现,您不限于以编程方式注册作用域。您还可以通过使用 CustomScopeConfigurer 类以声明方式进行 Scope 注册,如下例所示:

<?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="infra.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
      <map>
        <entry key="thread">
          <bean class="infra.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>
当您将 <aop:scoped-proxy/> 放置在 FactoryBean 实现的 <bean> 声明中时,作用域是工厂 bean 本身,而不是从 getObject() 返回的对象。