【译】IOC容器2

1.5. Bean Scope

当你创建一个Bean定义时,你创建了一个“配方”,用于创建该Bean定义(definition)是所定义的类的实际实例。Bean定义(definition)是一个“配方”的想法很重要,因为它意味着,就像一个类一样,你可以从一个“配方”中创建许多对象实例。

你不仅可以控制各种依赖和配置值,将其插入到从特定Bean定义创建的对象中,还可以控制从特定Bean定义创建的对象的scope。这种方法是强大而灵活的,因为你可以通过配置来选择你所创建的对象的scope,而不是在Java类级别上烘托出一个对象的scope。Bean可以被定义为部署在若干scope中的一个。Spring框架支持六个scope,其中四个只有在你使用Web感知(aware)的 ApplicationContext 时才可用。你也可以创建 一个自定义 scope

下表描述了支持的 scope。

Scope说明
singleton(默认情况下)为每个Spring IoC容器将单个Bean定义的Scope扩大到单个对象实例。
prototype将单个Bean定义的Scope扩大到任何数量的对象实例。
request将单个Bean定义的Scope扩大到单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的Bean实例,该实例是在单个Bean定义的基础上创建的。只在Web感知的Spring ApplicationContext 的上下文中有效。
session将单个Bean定义的Scope扩大到一个HTTP Session 的生命周期。只在Web感知的Spring ApplicationContext 的上下文中有效。
application将单个Bean定义的 Scope 扩大到 ServletContext 的生命周期中。只在Web感知的Spring ApplicationContext 的上下文中有效。
websocket将单个Bean定义的 Scope 扩大到 WebSocket 的生命周期。仅在具有Web感知的 Spring ApplicationContext 的上下文中有效。
一个 thread scope 是可用的,但默认情况下没有注册。欲了解更多信息,请参阅 SimpleThreadScope 的文档。关于如何注册这个或任何其他自定义 scope 的说明,请参见 使用自定义 Scope
1.5.1. Singleton Scope

只有一个单例 Bean 的共享实例被管理,所有对具有符合该Bean定义的ID的Bean的请求都会被Spring容器返回该特定的Bean实例。

换句话说,当你定义了一个Bean定义(define),并且它被定义为 singleton,Spring IoC容器就会为该Bean定义的对象创建一个确切的实例。这个单一的实例被存储在这种单体Bean的缓存中,所有后续的请求和对该命名Bean的引用都会返回缓存的对象。下面的图片显示了 singleton scope 是如何工作的。

singleton

Spring 的 singleton Bean概念与Gang of Four(GoF)模式书中定义的singleton模式不同。GoF singleton模式对对象的范围进行了硬编码,即每个ClassLoader创建一个且仅有一个特定类的实例。Spring单例的范围最好被描述为每个容器和每个bean。这意味着,如果你在一个Spring容器中为一个特定的类定义了一个Bean,Spring容器就会为该Bean定义的类创建一个且只有一个实例。Singleton scope 是Spring的默认 scope。要在XML中把一个Bean定义为singleton,你可以像下面的例子那样定义一个Bean。

<bean id="accountService" class="com.something.DefaultAccountService"/>
​
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2. Prototype Scope

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

下图说明了Spring prototype scope。

prototype

(数据访问对象(DAO)通常不被配置为 prototype,因为典型的DAO并不持有任何对话状态。对我们来说,重用 singleton 图的核心是比较容易的)。

下面的例子在XML中定义了一个 prototype bean。

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

与其他scope相比,Spring并不管理 prototype Bean的完整生命周期。容器对prototype对象进行实例化、配置和其他方面的组装,并将其交给客户端,而对该prototype实例没有进一步的记录。因此,尽管初始化生命周期回调方法在所有对象上被调用,而不考虑scope,但在prototype的情况下,配置的销毁生命周期回调不会被调用。客户端代码必须清理prototype scope 内的对象,并释放原prototype Bean持有的昂贵资源。为了让Spring容器释放由 prototype scopeBean 持有的资源,可以尝试使用自定义 Bean后处理器,它持有对需要清理的Bean的引用。

在某些方面,Spring容器在 prototype scope Bean 方面的作用是替代Java的 new 操作。所有超过该点的生命周期管理必须由客户端处理。(关于Spring容器中Bean的生命周期的详细信息,请参见 生命周期回调)。

1.5.3. singleton Bean 和 prototype bean 依赖

当你使用对 prototype Bean 有依赖的 singleton scope Bean时,请注意依赖关系是在实例化时解析的。因此,如果你将一个 prototype scope 的Bean依赖性注入到一个 singleton scope 的Bean中,一个新的 prototype Bean 被实例化,然后被依赖注入到 singleton Bean中。prototype 实例是唯一提供给 singleton scope Bean的实例。

然而,假设你想让 singleton scope 的Bean在运行时反复获得 prototype scope 的Bean的新实例。你不能将 prototype scope 的Bean 依赖注入到你的 singleton Bean中,因为这种注入只发生一次,当Spring容器实例化 singleton Bean 并解析和注入其依赖关系时。如果你在运行时需要一个新的 prototype Bean 实例不止一次,请参阅 方法注入

1.5.4. Request、 Session、 Application 和 WebSocket Scope

requestsessionapplicationwebsocket scope只有在你使用Web感知的Spring ApplicationContext 实现(如 XmlWebApplicationContext)时才可用。如果你将这些scope与常规的Spring IoC容器(如 ClassPathXmlApplicationContext)一起使用,就会抛出一个 IllegalStateException,抱怨有未知的Bean scope。

初始 Web 配置

为了支持Bean在 requestsessionapplicationWebsocket 级别的scope(Web scope 的Bean),在你定义Bean之前,需要一些小的初始配置。(对于标准作用域(singletonprototype)来说,这种初始设置是不需要的)。

你如何完成这个初始设置取决于你的特定Servlet环境。

如果你在Spring Web MVC中访问 scope 内的Bean,实际上是在一个由Spring DispatcherServlet 处理的请求(request)中,就不需要进行特别的设置。 DispatcherServlet 已经暴露了所有相关的状态。

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

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>

另外,如果你的监听器(listener)设置有问题,可以考虑使用Spring的 RequestContextFilter。过滤器(filter)的映射取决于周围的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>

DispatcherServletRequestContextListenerRequestContextFilter 都做了完全相同的事情,即把HTTP请求对象绑定到为该请求服务的 Thread。这使得 request scope 和 session scope 的Bean可以在调用链的更远处使用。列表显示了Web应用程序的过滤器部分。

Request scope

考虑以下用于Bean定义的XML配置。

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

Spring容器通过为每一个HTTP请求使用 loginAction Bean定义来创建 LoginAction Bean的新实例。也就是说,loginAction Bean在HTTP请求层面上是有 scope 的。你可以随心所欲地改变被创建的实例的内部状态,因为从同一个 loginAction Bean定义中创建的其他实例不会看到这些状态的变化。它们是针对单个请求的。当请求完成处理时,该请求所涉及的Bean会被丢弃。

当使用注解驱动(annotation-driven)的组件或Java配置时,@RequestScope 注解可以用来将一个组件分配到 request scope。下面的例子展示了如何做到这一点。

Java

Kotlin

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

考虑以下用于Bean定义的XML配置。

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

Spring容器通过使用 userPreferences Bean定义,在单个HTTP Session 的生命周期内创建一个新的 UserPreferences Bean实例。换句话说,userPreferences Bean在HTTP Session 级别上是有效的scope。与 request scope 的Bean一样,你可以随心所欲地改变被创建的实例的内部状态,要知道其他HTTP Session 实例也在使用从同一个 userPreferences Bean定义中创建的实例,它们不会看到这些状态的变化,因为它们是特定于单个HTTP Session。当HTTP Session 最终被丢弃时,作用于该特定HTTP Session 的bean也被丢弃。

当使用注解驱动(annotation-driven)的组件或Java配置时,你可以使用 @SessionScope 注解来将一个组件分配到 session scope。

Java

Kotlin

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

考虑以下用于Bean定义的XML配置。

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

Spring容器通过为整个Web应用程序使用一次 appPreferences Bean定义来创建 AppPreferences Bean的新实例。也就是说,appPreferences Bean是在 ServletContext 级别上的scope,并作为常规的 ServletContext 属性存储。这有点类似于Spring的 singleton Bean,但在两个重要方面有所不同。它是每个 ServletContext 的单例,而不是每个Spring ApplicationContext(在任何给定的Web应用程序中可能有几个),而且它实际上是暴露的,因此作为 ServletContext 属性可见。

当使用注解驱动(annotation-driven)的组件或Java配置时,你可以使用 @ApplicationScope 注解来将一个组件分配到 application scope。下面的例子显示了如何做到这一点。

Java

Kotlin

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
WebSocket Scope

WebSocket scope 与WebSocket会话的生命周期相关,适用于通过 WebSocket 实现的 STOMP 应用程序,详情请参见 WebSocket scope

作为依赖的 Scope Bean

Spring IoC容器不仅管理对象(Bean)的实例化,而且还管理协作者(或依赖)的连接。如果你想把(例如)一个HTTP request scope 的Bean注入到另一个时间较长的scope的Bean中,你可以选择注入一个AOP代理来代替这个 scope 的Bean。也就是说,你需要注入一个代理对象,它暴露了与 scope 对象相同的公共接口,但它也可以从相关的scope(如HTTP request)中检索到真正的目标对象,并将方法调用委托给真正的对象。

你也可以在 scope 为 singleton 的Bean之间使用 ,引用会经过一个可序列化的中间代理,因此能够在反序列化时重新获得目标 singleton Bean。当针对scope 为 `prototype` 的Bean声明 时,对共享代理的每个方法调用都会导致创建一个新的目标实例,然后调用被转发到该实例。另外,scope代理并不是以生命周期安全的方式从较短的scope访问Bean的唯一方法。你也可以将你的注入点(也就是构造器或设置器参数或自动注入的字段)声明为 ObjectFactory,允许在每次需要时通过 getObject() 调用来检索当前的实例—而不需要保留实例或单独存储它。作为一个扩展变量,你可以声明 ObjectProvider,它提供了几个额外的访问变体,包括 getIfAvailablegetIfUnique。JSR-330 的变体被称为 Provider,并在每次检索时使用 Provider 声明和相应的 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">
​
    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> 
    </bean>
​
    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>
定义代理的那一行。

要创建这样的代理,你需要在一个 scope Bean定义中插入一个子 元素(参见 [选择要创建的代理类型](https://springdoc.cn/spring/core.html#beans-factory-scopes-other-injection-proxies) 和 [基于 XML Schema 的配置](https://springdoc.cn/spring/core.html#core.appendix.xsd-schemas))。为什么在 `request`、`session` 和自定义 scope 层次上的Bean定义需要 元素?请考虑下面的 singleton Bean 定义,并与你需要为上述 scope 定义的内容进行对比(注意,下面的 userPreferences Bean定义是不完整的)。

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
​
<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

在前面的例子中,singleton Bean(userManager)被注入了对HTTP Session scope Bean(userPreferences)的引用。这里突出的一点是 userManager Bean是一个 singleton:它在每个容器中只被实例化一次,它的依赖关系(在这种情况下只有一个,即 userPreferences Bean)也只被注入一次。这意味着 userManager Bean只对完全相同的 userPreferences 对象(也就是它最初被注入的对象)进行操作。

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

因此,在将 request scope 和 session scope 的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创建代理时,会创建一个基于CGLIB的类代理。

CGLIB代理只拦截public方法的调用! 不要在这样的代理上调用非public的方法。它们不会被委托给实际scope内的目标对象。

另外,你也可以通过为 ` 元素的proxy-target-class属性的值指定false` 来配置Spring容器,使其为这种 scope 内的Bean创建基于JDK接口的标准代理。使用基于JDK接口的代理意味着你不需要在你的应用程序 classpath 中使用额外的库来影响这种代理。然而,这也意味着scope Bean的类必须至少实现一个接口,并且scope 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>

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

1.5.5. 自定义 Scope

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

创建自定义 Scope

为了将你的自定义scope集成到 Spring 容器中,你需要实现 org.springframework.beans.factory.config.Scope 接口,本节将介绍该接口。要了解如何实现你自己的scope,请参阅 Spring 框架本身提供的 Scope 实现,以及 Scope javadoc,其中更详细地解释了你需要实现的方法。

Scope 接口有四个方法来从scope中获取对象,从scope中移除对象,以及让对象被销毁。

例如,session scope的实现会返回 session scope 的 bean(如果它不存在,该方法会返回一个新的 bean 实例,在把它绑定到 session 上供将来引用)。下面的方法从底层scope返回对象。

Java

Kotlin

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

例如,session scope的实现是将session scope的Bean从底层session中移除。该对象应该被返回,但是如果没有找到指定名称的对象,你可以返回 null。下面的方法将对象从底层scope中删除。

Java

Kotlin

Object remove(String name)

下面的方法注册了一个callback,当scope被销毁或scope中的指定对象被销毁时,该callback应该被调用。

Java

Kotlin

void registerDestructionCallback(String name, Runnable destructionCallback)

请参阅 javadoc 或Spring scope 的实现,以了解更多关于销毁callback的信息。

下面的方法获得底层scope的conversation id。

Java

Kotlin

String getConversationId()

这个 id 对每个 scope 都是不同的。对于一个 session scope 的实现,这个 id 可以是 session id。

使用自定义 Scope

在你编写并测试了一个或多个自定义 Scope 实现之后,你需要让 Spring 容器知道你的新 Scope。下面的方法是向Spring容器注册新 Scope 的核心方法。

Java

Kotlin

void registerScope(String scopeName, Scope scope);

这个方法是在 ConfigurableBeanFactory 接口上声明的,它可以通过Spring的大多数具体 ApplicationContext 实现上的 BeanFactory 属性获得。

registerScope(..) 方法的第一个参数是与一个 scope 相关的唯一的名称。在 Spring 容器本身中这种名称的例子是 singletonprototyperegisterScope(..) 方法的第二个参数是你希望注册和使用的自定义 Scope 实现的实际实例。

假设你写了你的自定义 Scope 的实现,然后按下一个例子所示注册它。

下一个例子使用了 SimpleThreadScope,它包含在Spring中,但默认没有注册。对于你自己的自定义 Scope 实现,其说明是一样的。

Java

Kotlin

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

然后你可以创建符合你的自定义 Scope 的 scope 规则的bean定义,如下所示。

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

有了自定义的 Scope 实现,你就不局限于以编程方式注册该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="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` 实现的 声明中时,是 factory bean 本身被限定了scope,而不是从 getObject() 返回的对象。

1.6. 自定义Bean的性质(Nature)

Spring框架提供了许多接口,你可以用它们来定制Bean的性质。本节将它们分组如下。

1.6.1. 生命周期回调

为了与容器对Bean生命周期的管理进行交互,你可以实现Spring InitializingBeanDisposableBean 接口。容器为前者调用 afterPropertiesSet(),为后者调用 destroy(),让Bean在初始化和销毁你的Bean时执行某些动作。

JSR-250的 @PostConstruct@PreDestroy 注解通常被认为是在现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注解意味着你的Bean不会被耦合到Spring特定的接口。详情请参见 使用 使用 @PostConstruct 和 @PreDestroy。如果你不想使用JSR-250注解,但你仍然想消除耦合,可以考虑用 init-methoddestroy-method bean 定义元数据。

在内部,Spring框架使用 BeanPostProcessor 实现来处理它能找到的任何回调接口并调用相应的方法。如果你需要自定义功能或其他Spring默认不提供的生命周期行为,你可以自己实现一个 BeanPostProcessor。欲了解更多信息,请参见 容器扩展点

除了初始化和销毁回调外,Spring管理的对象还可以实现 Lifecycle 接口,以便这些对象能够参与启动和关闭过程,这是由容器自己的生命周期驱动的。

生命周期回调接口在本节中描述。

初始化回调

org.springframework.beans.factory.InitializingBean 接口让Bean在容器对Bean设置了所有必要的属性后执行初始化工作。InitializingBean 接口指定了一个方法。

void afterPropertiesSet() throws Exception;

我们建议你不要使用 InitializingBean 接口,因为它不必要地将代码与Spring耦合。另外,我们建议使用 @PostConstruct 注解或指定一个POJO初始化方法。在基于XML的配置元数据中,你可以使用 init-method 属性来指定具有 void 无参数签名的方法的名称。对于Java配置,你可以使用 @BeaninitMethod 属性。参见 接收生命周期的回调。考虑一下下面的例子。

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

Java

Kotlin

public class ExampleBean {

    public void init() {
        // do some initialization work
    }
}

前面的例子与下面的例子(由两个列表组成)的效果几乎完全相同。

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

Java

Kotlin

public class AnotherExampleBean implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        // do some initialization work
    }
}

然而,前面两个例子中的第一个并没有将代码与Spring耦合。

销毁回调

实现 org.springframework.beans.factory.DisposableBean 接口可以让Bean在包含它的容器被销毁时获得一个回调。DisposableBean 接口指定了一个方法。

void destroy() throws Exception;

我们建议你不要使用 DisposableBean 回调接口,因为它不必要地将代码耦合到Spring。另外,我们建议使用 @PreDestroy 注解或指定一个bean定义所支持的通用方法。对于基于XML的配置元数据,你可以使用 ` 上的destroy-method属性。使用Java配置,你可以使用@BeandestroyMethod` 属性。参见接收生命周期的回调。考虑一下下面的定义。

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>

Java

Kotlin

public class ExampleBean {
​
    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

前面的定义与下面的定义几乎有完全相同的效果。

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

Java

Kotlin

public class AnotherExampleBean implements DisposableBean {

    @Override
    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

然而,前面两个定义中的第一个并没有将代码与Spring耦合。

你可以给 元素的 `destroy-method` 属性分配一个特殊的 `(inferred)` 值,它指示Spring自动检测特定bean类上的public `close` 或 `shutdown` 方法。(任何实现了 `java.lang.AutoCloseable` 或 `java.io.Closeable` 的类都可以匹配)。你也可以在 元素的 default-destroy-method 属性上设置这个特殊的 (inferred) 值,将这个行为应用于整个Bean集合(参见 默认的初始化和销毁方法)。请注意,这是用Java配置的默认行为。
默认的初始化和销毁方法

当你写初始化和销毁方法回调时,如果不使用Spring特定的 InitializingBeanDisposableBean 回调接口,你通常会写一些名称为 init()initialize()dispose() 等的方法。理想情况下,这种生命周期回调方法的名称在整个项目中是标准化的,这样所有的开发者都会使用相同的方法名称,确保一致性。

你可以将Spring容器配置为在每个Bean上 "寻找" 命名的初始化和销毁回调方法名称。这意味着你,作为应用开发者,可以编写你的应用类并使用名为 init() 的初始化回调,而不必为每个Bean定义配置 init-method="init" 属性。当Bean被创建时,Spring IoC容器会调用该方法(并且符合 之前描述 的标准生命周期回调约定)。这一特性也为初始化和销毁方法的回调执行了一致的命名规则。

假设你的初始化回调方法被命名为 init(),你的销毁回调方法被命名为 destroy()。那么你的类就类似于下面这个例子中的类。

Java

Kotlin

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }

    // this is (unsurprisingly) the initialization callback method
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }
}

然后你可以在一个类似于以下的bean中使用该类。

<beans default-init-method="init">
​
    <bean id="blogService" class="com.something.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>
​
</beans>

顶层 ` 元素属性中default-init-method属性的存在会使Spring IoC容器识别出Bean类中名为init` 的方法作为初始化方法的回调。当一个Bean被创建和装配时,如果Bean类有这样的方法,它就会在适当的时候被调用。

你可以通过使用顶层 ` 元素上的default-destroy-method属性,类似地配置destroy` 方法回调(在XML中,也就是)。

如果现有的Bean类已经有了与惯例不同的回调方法,你可以通过使用 > 本身的init-methoddestroy-method` 属性来指定(在XML中)方法的名称,从而覆盖默认值。

Spring容器保证在Bean被提供了所有的依赖关系后立即调用配置的初始化回调。因此,初始化回调是在原始Bean引用上调用的,这意味着AOP拦截器等还没有应用到Bean上。首先完全创建一个目标Bean,然后应用一个带有拦截器链的AOP代理(比如说)。如果目标Bean和代理是分开定义的,你的代码甚至可以绕过代理,与原始的目标Bean进行交互。因此,将拦截器应用于 init 方法是不一致的,因为这样做会将目标Bean的生命周期与它的代理或拦截器联系起来,当你的代码直接与原始目标Bean交互时,会留下奇怪的语义。

结合生命周期机制

从Spring 2.5开始,你有三个选项来控制Bean的生命周期行为。

如果为一个bean配置了多个生命周期机制,并且每个机制都配置了不同的方法名称,那么每个配置的方法都会按照本说明后面列出的顺序运行。然而,如果同一方法名称被配置—例如,init() 为一个初始化方法—用于多个这些生命周期机制,则该方法将被运行一次,如 上一节 所解释的。

为同一个Bean配置的多个生命周期机制,具有不同的初始化方法,其调用方式如下。

  1. 注解了 @PostConstruct 的方法。

  2. afterPropertiesSet(),如 InitializingBean 回调接口所定义。

  3. 一个自定义配置的 init() 方法。

销毁方法的调用顺序是一样的。

  1. 注解了 @PreDestroy 的方法。

  2. destroy(),正如 DisposableBean 回调接口所定义的那样。

  3. 一个自定义配置的 destroy() 方法。

启动和关闭的回调

Lifecycle 接口定义了任何有自己的生命周期要求的对象的基本方法(如启动和停止一些后台进程)。

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何Spring管理的对象都可以实现 Lifecycle 接口。然后,当 ApplicationContext 本身收到启动和停止信号时(例如,在运行时的停止/重启场景),它将这些调用级联到定义在该上下文中的所有 Lifecycle 实现。它通过委托给一个 LifecycleProcessor 来实现,如下表所示。

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessor 本身就实现了 Lifecycle 接口。它还添加了另外两个方法来对 context 的刷新和关闭做出反应。

请注意,常规的 org.springframework.context.Lifecycle 接口是一个明确的start和stop通知的普通约定,并不意味着在上下文刷新时自动启动。如果要对特定Bean的自动启动进行细粒度控制(包括启动阶段),可以考虑实现 org.springframework.context.SmartLifecycle 来代替。另外,请注意,stop通知并不保证在销毁之前出现。在定期关机时,所有的 Lifecycle Bean都会在一般的销毁回调被传播之前首先收到stop通知。然而,在上下文生命周期中的热刷新或stop刷新尝试时,只有销毁方法被调用。

启动和关闭调用的顺序可能很重要。如果任何两个对象之间存在 "依赖" 关系,被依赖方在其依赖方之后启动,在其依赖方之前停止。然而,有时候,直接的依赖关系是未知的。你可能只知道某种类型的对象应该在另一种类型的对象之前启动。在这些情况下, SmartLifecycle 接口定义了另一个选项,即其超接口 Phased 上定义的 getPhase() 方法。下面的列表显示了 Phased 接口的定义。

public interface Phased {

    int getPhase();
}

下面列出了 SmartLifecycle 接口的定义。

public interface SmartLifecycle extends Lifecycle, Phased {
​
    boolean isAutoStartup();
​
    void stop(Runnable callback);
}

启动时,phase最低的对象先启动。当停止时,遵循相反的顺序。因此,一个实现了 SmartLifecycle 并且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将是最先启动和最后停止的对象。在 spectrum 的另一端,一个 Integer.MAX_VALUE 的 phase 值将表明该对象应该最后启动并首先停止(可能是因为它依赖于其他进程的运行)。在考虑 phase 值时,同样重要的是要知道,任何没有实现 SmartLifecycle 的 "正常" Lifecycle 对象的默认 phase 是 0。 因此,任何负的 phase 值表示一个对象应该在那些标准组件之前开始(并在它们之后停止)。反之,任何正的 phase 值也是如此。

SmartLifecycle 定义的 stop 方法接受一个回调。任何实现都必须在该实现的关闭过程完成后调用该回调的 run() 方法。这在必要时可以实现异步关机,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 会等待每个阶段内的对象组调用该回调,直到其超时值。每个阶段的默认超时是 30 秒。你可以通过在上下文中定义一个名为 lifecycleProcessor 的bean来覆盖默认的生命周期处理器实例。如果你只想修改超时时间,定义以下内容就足够了。

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

如前所述,LifecycleProcessor 接口也定义了用于刷新和关闭上下文(context )的回调方法。后者驱动关闭过程,就像明确调用 stop() 一样,但它发生在上下文关闭的时候。另一方面,"refresh" 回调方法实现了 SmartLifecycle Bean的另一个特性。当上下文被刷新时(在所有对象都被实例化和初始化后),该回调被调用。这时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法所返回的布尔值。如果为 true,该对象将在此时启动,而不是等待上下文或其自身 start() 方法的显式调用(与上下文刷新不同,上下文的启动不会自动发生在标准的上下文实现中)。如前所述,phase 值和任何 "依赖" 关系决定了启动的顺序。

在非Web应用中优雅地关闭Spring IoC容器
本节仅适用于非Web应用。Spring的基于Web的 ApplicationContext 实现已经有代码可以在相关Web应用关闭时优雅地关闭Spring IoC容器。

如果你在非web应用环境中使用Spring的IoC容器(例如,在客户端桌面环境中),请向JVM注册一个shutdown hook。这样做可以确保优雅地关闭,并在你的singleton Bean上调用相关的 destroy 方法,从而释放所有资源。你仍然必须正确配置和实现这些 destroy 回调。

要注册一个 shutdown hook,请调用 registerShutdownHook() 方法,该方法在 ConfigurableApplicationContext 接口上声明,如下例所示。

Java

Kotlin

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...
    }
}
1.6.2. ApplicationContextAwareBeanNameAware

ApplicationContext 创建一个实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,该实例被提供给该 ApplicationContext 的引用。下面的列表显示了 ApplicationContextAware 接口的定义。

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,Bean可以通过 ApplicationContext 接口或通过将引用转换为该接口的已知子类(如 ConfigurableApplicationContext,它暴露了额外的功能),以编程方式操作创建它们的 ApplicationContext。一个用途是对其他Bean进行编程式检索。有时这种能力是很有用的。然而,一般来说,你应该避免这样做,因为它将代码与Spring耦合在一起,并且不遵循控制反转(Inversion of Control)的风格,即合作者作为属性提供给Bean。ApplicationContext 的其他方法提供了对文件资源的访问,发布应用程序事件,以及访问 MessageSource。这些额外的功能将在 ApplicationContext 的附加功能 中描述。

Autowire 是获得对 ApplicationContext 引用的另一种选择。传统的 constructorbyType 自动注入模式(如 注入协作者(Autowiring Collaborators) 中所述)可以分别为构造器参数或设 setter 方法参数提供 ApplicationContext 类型的依赖。为了获得更多的灵活性,包括自动注入字段和多个参数方法的能力,请使用基于注解的自动注入功能。如果你这样做,ApplicationContext 将被自动注入到字段、构造函数参数或方法参数中,如果有关字段、构造函数或方法带有 @Autowired 注解,则期望 ApplicationContext 类型。更多信息请参见 使用 @Autowired

ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类时,该类被提供给其相关对象定义中定义的名称的引用。下面的列表显示了 BeanNameAware 接口的定义。

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

这个回调是在正常的Bean属性之后,但在 InitializingBean.afterPropertiesSet() 或自定义 init-method 等初始化回调之前调用的。

1.6.3. 其他 Aware 接口

除了 ApplicationContextAwareBeanNameAware前面讨论过),Spring还提供了一系列的 Aware 回调接口,让Bean向容器表明它们需要某种基础设施的依赖性。一般来说,名称表示依赖关系的类型。下表总结了最重要的 Aware 接口。

接口名称注入的依赖性解释
ApplicationContextAware声明 ApplicationContextApplicationContextAware 和 BeanNameAware
ApplicationEventPublisherAware封装了 ApplicationContext 的 Event publisher 。ApplicationContext 的附加功能
BeanClassLoaderAware用来加载Bean类的类加载器(Class loader)。实例化 Bean
BeanFactoryAware声明 BeanFactoryBeanFactory API
BeanNameAware声明Bean的名称。ApplicationContextAware 和 BeanNameAware
LoadTimeWeaverAware定义了用于在加载时处理类定义的织入点。在Spring框架中用AspectJ进行加载时织入(Load-time Weaving)
MessageSourceAware配置解析消息的策略(支持参数化和国际化)。ApplicationContext 的附加功能
NotificationPublisherAwareSpring JMX notification publisher。Notifications
ResourceLoaderAware配置的加载器用于低级别的资源访问。资源(Resources)
ServletConfigAware容器所运行的当前 ServletConfig。仅在 Web 感知的 Spring ApplicationContext 中有效。Spring MVC

请再次注意,使用这些接口会将你的代码与Spring API捆绑在一起,并且不遵循反转控制的风格。因此,我们建议那些需要对容器进行编程访问的基础设施Bean使用这些接口。

1.7. Bean 定义(Definition)的继承

一个Bean定义可以包含很多配置信息,包括构造函数参数、属性值和容器特有的信息,如初始化方法、静态工厂方法名称等等。一个子Bean定义从父定义继承配置数据。子定义可以覆盖一些值或根据需要添加其他值。使用父Bean定义和子Bean定义可以节省大量的打字工作。有效地,这是一种模板化的形式。

如果你以编程方式处理 ApplicationContext 接口,子bean定义由 ChildBeanDefinition 类表示。大多数用户不会在这个层面上与他们一起工作。相反,他们在 ClassPathXmlApplicationContext 这样的类中声明性地配置Bean定义。当你使用基于XML的配置元数据时,你可以通过使用 parent 属性来指示子Bean定义,将父Bean指定为这个属性的值。下面的例子显示了如何做到这一点。

<bean id="inheritedTestBean" abstract="true"
        class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
        class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBean" init-method="initialize">  
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>
注意 parent 属性。

如果没有指定,子Bean定义会使用父定义中的Bean类,但也可以覆盖它。在后一种情况下,子Bean类必须与父类兼容(也就是说,它必须接受父类的属性值)。

子Bean定义从父级继承scope、构造函数参数值、属性值和方法重写,并可以选择添加新的值。你指定的任何scope、初始化方法、销毁(destroy)方法或 static 工厂方法设置都会覆盖相应的父类设置。

其余的设置总是来自于子定义:依赖、自动注入模式、依赖检查、singleton和懒加载。

前面的例子通过使用 abstract 属性明确地将父类Bean定义标记为抽象的。如果父定义没有指定一个类,就需要明确地将父Bean定义标记为抽象的,如下例所示。

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>
​
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

父类Bean不能被单独实例化,因为它是不完整的,而且它也被明确标记为 abstract 的。当一个定义是 abstract 的,它只能作为一个纯模板Bean定义使用,作为子定义的父定义。试图单独使用这样的 abstract 父类 bean,通过将其作为另一个 bean 的 ref 属性来引用,或者用父类 bean 的 ID 进行显式 getBean() 调用,会返回一个错误。同样地,容器内部的 preInstantiateSingletons() 方法也会忽略被定义为抽象的 bean 定义。

ApplicationContext 默认预设了所有的singleton。因此,重要的是(至少对于singleton Bean来说),如果你有一个(父)Bean定义,你打算只作为模板使用,并且这个定义指定了一个类,你必须确保将 abstract 属性设置为 true,否则应用上下文将实际(试图)预实化 abstract Bean。

1.8. 容器扩展点

通常情况下,应用程序开发人员不需要对 ApplicationContext 实现类进行子类化。相反,Spring IoC容器可以通过插入特殊集成接口的实现来进行扩展。接下来的几节将描述这些集成接口。

1.8.1. 使用 BeanPostProcessor 自定义 Bean

BeanPostProcessor 接口定义了回调方法,你可以实现这些方法来提供你自己的(或覆盖容器的默认)实例化逻辑、依赖性解析逻辑等。如果你想在Spring容器完成实例化、配置和初始化Bean之后实现一些自定义逻辑,你可以插入一个或多个自定义 BeanPostProcessor 实现。

你可以配置多个 BeanPostProcessor 实例,你可以通过设置 order 属性控制这些 BeanPostProcessor 实例的运行顺序。只有当 BeanPostProcessor 实现了 Ordered 接口时,你才能设置这个属性。如果你编写自己的 BeanPostProcessor,你也应该考虑实现 Ordered 接口。关于进一步的细节,请参阅 BeanPostProcessorOrdered 接口的 javadoc。也请参见关于 BeanPostProcessor 实例的程序化注册 的说明。

BeanPostProcessor 实例对Bean(或对象)实例进行操作。也就是说,Spring IoC容器实例化一个Bean实例,然后由 BeanPostProcessor 实例来完成其工作。BeanPostProcessor 实例是按容器范围的。这只有在你使用容器层次结构时才有意义。如果你在一个容器中定义了一个 BeanPostProcessor,它只对该容器中的Bean进行后处理。换句话说,在一个容器中定义的 BeanPostProcessor 不会对另一个容器中定义的 BeanPostProcessor 进行后处理,即使两个容器都是同一层次结构的一部分。要改变实际的Bean定义(即定义Bean的蓝图),你需要使用 BeanFactoryPostProcessor,如 用 BeanFactoryPostProcessor 定制配置元数据 中所述。

org.springframework.beans.factory.config.BeanPostProcessor 接口正好由两个回调方法组成。当这样的类被注册为容器的后处理器时,对于容器创建的每个 bean 实例,后处理器在容器初始化方法(如 InitializingBean.afterPropertiesSet() 或任何已声明的 init 方法)被调用之前和任何 bean 初始化回调之后都会从容器获得一个回调。后处理程序可以对Bean实例采取任何行动,包括完全忽略回调。bean类后处理器通常会检查回调接口,或者用代理来包装bean类。一些Spring AOP基础设施类被实现为Bean后处理器,以提供代理封装逻辑。

ApplicationContext 会自动检测在配置元数据中定义的实现 BeanPostProcessor 接口的任何Bean。ApplicationContext 将这些 bean 注册为后处理器,以便以后在 bean 创建时可以调用它们。Bean后处理器可以像其他Bean一样被部署在容器中。

请注意,通过在配置类上使用 @Bean 工厂方法来声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,明确表示该 Bean 的后处理性质。否则,ApplicationContext 无法在完全创建它之前按类型自动检测它。由于 BeanPostProcessor 需要尽早被实例化,以便应用于上下文中其他Bean的初始化,所以这种早期的类型检测是至关重要的。

以编程方式注册 BeanPostProcessor 实例虽然推荐的 BeanPostProcessor 注册方法是通过 ApplicationContext 自动检测(如前所述),但你可以通过使用 addBeanPostProcessor 方法,针对 ConfigurableBeanFactory 以编程方式注册它们。当你需要在注册前评估条件逻辑或甚至在一个层次结构中跨上下文复制Bean Post处理器时,这可能很有用。然而,请注意,以编程方式添加的 BeanPostProcessor 实例并不尊重 Ordered 接口。这里,是注册的顺序决定了执行的顺序。还要注意的是,以编程方式注册的 BeanPostProcessor 实例总是在通过自动检测注册的实例之前被处理,而不考虑任何明确的顺序。
BeanPostProcessor 实例和AOP自动代理实现了 BeanPostProcessor 接口的类是特殊的,会被容器区别对待。所有 BeanPostProcessor 实例和它们直接引用的Bean在启动时被实例化,作为 ApplicationContext 特殊启动阶段的一部分。接下来,所有的 BeanPostProcessor 实例被分类注册,并应用于容器中的所有其他Bean。因为AOP的自动代理是作为 BeanPostProcessor 本身实现的,所以 BeanPostProcessor 实例和它们直接引用的Bean都不符合自动代理的条件,因此,没有切面被织入进去。对于任何这样的Bean,你应该看到一个信息性的日志消息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)。如果你通过使用自动注入或 @Resource(可能会退回到自动注入)将 BeanPostProcessor 连接起来,Spring在搜索类型匹配的依赖候选者时可能会访问意想不到的Bean,因此,使它们没有资格进行自动代理或其他种类的Bean后处理。例如,如果你有一个用 @Resource 注解的依赖,其中字段或setter的名称不直接对应于Bean的声明名称,并且没有使用name属性,Spring会访问其他Bean以通过类型匹配它们。

下面的例子展示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例。

示例: Hello World, BeanPostProcessor

这第一个例子说明了基本用法。这个例子展示了一个自定义的 BeanPostProcessor 实现,它在容器创建每个Bean时调用 toString() 方法,并将结果字符串打印到系统控制台。

下面的列表显示了自定义 BeanPostProcessor 实现类的定义。

Java

Kotlin

package scripting;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}

下面的bean元素使用了 InstantiationTracingBeanPostProcessor

<?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:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang
        https://www.springframework.org/schema/lang/spring-lang.xsd">

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!--
    when the above bean (messenger) is instantiated, this custom
    BeanPostProcessor implementation will output the fact to the system console
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意 InstantiationTracingBeanPostProcessor 是如何被定义的。它甚至没有名字,而且因为它是一个Bean,它可以像其他Bean一样被依赖注入。(前面的配置还定义了一个由Groovy脚本支持的Bean。Spring的动态语言支持详见 动态语言支持 一章)。

下面的Java应用程序运行前面的代码和配置。

Java

Kotlin

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
        Messenger messenger = ctx.getBean("messenger", Messenger.class);
        System.out.println(messenger);
    }

}

前面的应用程序的输出类似于以下内容。

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
示例: AutowiredAnnotationBeanPostProcessor

将回调接口或注解与自定义 BeanPostProcessor 实现结合起来使用,是扩展Spring IoC容器的一种常见手段。一个例子是Spring的 AutowiredAnnotationBeanPostProcessor — 一个 BeanPostProcessor 实现,它与 Spring distribution 一起,自动注入注解字段、setter方法和任意的配置方法。

1.8.2. 用 BeanFactoryPostProcessor 定制配置元数据

我们看的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口的语义与 BeanPostProcessor 的语义相似,但有一个主要区别。BeanFactoryPostProcessor 对Bean配置元数据进行操作。也就是说,Spring IoC容器让 BeanFactoryPostProcessor 读取配置元数据,并在容器实例化 BeanFactoryPostProcessor 实例以外的任何Bean之前对其进行潜在的修改。

你可以配置多个 BeanFactoryPostProcessor 实例,你可以通过设置 order 属性控制这些 BeanFactoryPostProcessor 实例的运行顺序。然而,只有当 BeanFactoryPostProcessor 实现了 Ordered 接口时,你才能设置这个属性。如果你编写自己的 BeanFactoryPostProcessor,你也应该考虑实现 Ordered 接口。请参阅 BeanFactoryPostProcessorOrdered 接口的 javadoc,了解更多细节。

如果你想改变实际的Bean实例(即从配置元数据中创建的对象),那么你需要使用 BeanPostProcessor(如前面 使用 BeanPostProcessor 自定义 Bean 中的描述)。虽然在技术上可以在 BeanFactoryPostProcessor 中处理Bean实例(例如,通过使用 BeanFactory.getBean()),但这样做会导致过早的Bean实例化,违反了标准容器的生命周期。这可能会导致负面的副作用,比如绕过Bean的后期处理。另外,BeanFactoryPostProcessor 实例是按容器范围的。这只有在你使用容器层次结构时才有意义。如果你在一个容器中定义了一个 BeanFactoryPostProcessor,它将只应用于该容器中的Bean定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例进行后处理,即使两个容器都是同一层次结构的一部分。

当Bean工厂在 ApplicationContext 内声明时,会自动运行Bean工厂后处理器,以便对定义容器的配置元数据进行修改。Spring包括一些预定义的Bean Factory后处理器,如 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer。你也可以使用一个自定义的 BeanFactoryPostProcessor--例如,注册自定义的属性编辑器(property editor)。

ApplicationContext 会自动检测被部署到其中的实现了 BeanFactoryPostProcessor 接口的任何Bean。它在适当的时候将这些Bean用作Bean Factory后处理器。你可以像部署其他Bean一样部署这些后处理器Bean。

BeanPostProcessor 一样,你通常不希望将 BeanFactoryPostProcessor 配置为懒加载。如果没有其他bean引用 Bean(Factory)PostProcessor,该 PostProcessor 将根本不会被实例化。因此,将其标记为懒加载将被忽略,即使你在 ` 元素的声明中把default-lazy-init属性设置为trueBean(Factory)PostProcessor` 也会被急切地实例化。
示例: 类名替换 PropertySourcesPlaceholderConfigurer

你可以使用 PropertySourcesPlaceholderConfigurer,通过使用标准的Java Properties 格式,将Bean定义中的属性值外化到一个单独的文件中。这样做使部署应用程序的人能够定制特定环境的属性,如数据库URL和密码,而不需要修改容器的主要XML定义文件或文件的复杂性或风险。

考虑以下基于XML的配置元数据片段,其中定义了一个具有占位值的 DataSource

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

这个例子显示了从外部 Properties 文件配置的属性。在运行时, PropertySourcesPlaceholderConfigurer 被应用到元数据中,取代了 DataSource 的一些属性。要替换的值被指定为 ${property-name} 形式的占位符,它遵循Ant和log4j以及JSP EL的风格。

实际的数值来自另一个文件,是标准的Java Properties 格式。

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

因此,${jdbc.username} 字符串在运行时被替换为值 'sa',这同样适用于其他与properties文件中的key相匹配的占位符值。 PropertySourcesPlaceholderConfigurer 会检查Bean定义的大多数属性中的占位符。此外,你可以自定义占位符的前缀(prefix)和后缀(suffix)。

通过Spring 2.5中引入的 context 命名空间,你可以用一个专门的配置元素来配置属性占位符。你可以在 location 属性中提供一个或多个位置作为逗号分隔的列表,如下例所示。

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>

PropertySourcesPlaceholderConfigurer 不仅会在你指定的 Properties 文件中寻找属性。默认情况下,如果它不能在指定的 properties 文件中找到一个属性,它会检查 Spring Environment properties 和常规Java System properties。

你可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,当你必须在运行时挑选一个特定的实现类时,这有时很有用。下面的例子显示了如何做到这一点。classpath:com/something/strategy.properties custom.strategy.class=com.something.DefaultStrategy如果该类在运行时不能被解析为一个有效的类,那么在即将创建Bean时,也就是在非 lazy-init Bean 的 ApplicationContextpreInstantiateSingletons() 阶段,Bean的解析会失败。
示例:PropertyOverrideConfigurer

PropertyOverrideConfigurer 是另一个 Bean factory 的后处理器,与 PropertySourcesPlaceholderConfigurer 相似,但与后者不同的是,原始定义可以为Bean属性设置默认值或根本没有值。如果覆盖的 Properties 文件中没有某个Bean属性的条目,就会使用默认的上下文定义。

请注意,Bean定义并不知道被覆盖,所以从XML定义文件中并不能立即看出正在使用覆盖的配置器(override configurer)。如果有多个 PropertyOverrideConfigurer 实例为同一个Bean属性定义不同的值,由于覆盖机制的存在,最后一个实例获胜。

Properties 文件配置行的格式如下。

beanName.property=value

下面的列表显示了一个格式的例子。

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

这个示例文件可用于包含一个名为 dataSource 的Bean的容器定义,该Bean具有 driverurl 属性。

也支持复合属性名,只要路径中的每个组件,除了被重载的最终属性外,都已经是非null的(大概是被构造函数初始化了)。在下面的例子中,tom Bean的 fred 属性的 bob 属性的 sammy 属性被设置为标量值 123

tom.fred.bob.sammy=123
指定的覆盖值总是字面值。它们不被翻译成 bean 引用。当 XML Bean 定义中的原始值指定了一个 bean 引用时,这一约定也适用。

通过Spring 2.5中引入的 context 命名空间,可以用一个专门的配置元素来配置属性重写,如下例所示。

<context:property-override location="classpath:override.properties"/>
1.8.3. 用 FactoryBean 自定义实例化逻辑

你可以为那些本身就是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。

FactoryBean 接口是Spring IoC容器实例化逻辑的一个可插入点。如果你有复杂的初始化代码,最好用Java来表达,而不是用(潜在的)冗长的XML来表达,你可以创建自己的 FactoryBean,将复杂的初始化写入该类中,然后将你的自定义 FactoryBean 插入容器中。

FactoryBean 接口提供三个方法。

  • T getObject(): 返回本工厂创建的对象的一个实例。该实例可能会被共享,这取决于该工厂是返回singleton还是prototype。

  • boolean isSingleton(): 如果这个 FactoryBean 返回 singleton,则返回 true,否则返回 false。这个方法的默认实现会返回 true

  • Class getObjectType(): 返回由 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null

在Spring框架中,FactoryBean 的概念和接口在很多地方都有使用。Spring本身就有50多个 FactoryBean 接口的实现。

当你需要向容器索取一个实际的 FactoryBean 实例而不是它产生的Bean时,在调用 ApplicationContextgetBean() 方法时,在Bean的 id 前加上安培符号(&)。因此,对于一个 idmyBeanFactoryBean,在容器上调用 getBean("myBean") 会返回 FactoryBean 的产物,而调用 getBean("&myBean") 会返回 FactoryBean 实例本身。

1.9. 基于注解的容器配置

在配置Spring时,注解是否比XML更好?

基于注解的配置的引入提出了这样一个问题:这种方法是否比XML "更好"。简短的回答是 "视情况而定"。长的答案是,每种方法都有它的优点和缺点,而且,通常是由开发者来决定哪种策略更适合他们。由于它们的定义方式,注解在其声明中提供了大量的上下文,导致了更短、更简洁的配置。然而,XML擅长于在不触及源代码或重新编译的情况下对组件进行注入。一些开发者更喜欢在源码附近注入,而另一些人则认为带注解的类不再是POJO,此外,配置变得分散,更难控制。

不管是哪种选择,Spring都能适应这两种风格,甚至将它们混合在一起。值得指出的是,通过其 JavaConfig 选项,Spring允许以非侵入性的方式使用注解,而不触及目标组件的源代码,在工具方面,所有的配置风格都被 Spring Tools for Eclipse、Visual Studio Code 和 Theia 所支持。

基于注解的配置提供了XML设置的替代方案,它依靠字节码元数据来注入组件而不是XML声明。开发者通过在相关的类、方法或字段声明上使用注解,将配置移入组件类本身,而不是使用XML来描述bean的装配。正如 示例: AutowiredAnnotationBeanPostProcessor 中提到的,将 BeanPostProcessor 与注解结合使用是扩展Spring IoC容器的常见手段。例如,@Autowired 注解提供了与 注入协作者(Autowiring Collaborators) 中所描述的相同的功能,但控制范围更细,适用性更广。此外,Spring还提供了对JSR-250注解的支持,如 @PostConstruct@PreDestroy,以及对JSR-330(Java的依赖注入)注解的支持,该注解包含在 jakarta.inject 包中,如 @Inject@Named。关于这些注解的细节可以在 相关章节 中找到。

注解注入是在XML注入之前进行的。因此,XML配置覆盖了通过这两种方法注入的属性的注解。

一如既往,你可以将后处理器注册为单独的Bean定义,但也可以通过在基于XML的Spring配置中包含以下标签来隐式注册(注意包含 context 命名空间)。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
​
    <context:annotation-config/>
​
</beans>

`` 元素隐含地注册了下列后处理器( post-processor)。

只在定义它的同一应用上下文(application context)中寻找对Bean的注解。这意味着,如果你把 放在 DispatcherServletWebApplicationContext 中,它只检查 controller 中的 @Autowired Bean,而不是 service。参见 DispatcherServlet 以了解更多信息。
1.9.1. 使用 @Autowired
JSR 330的 @Inject 注解可以代替Spring的 @Autowired 注解在本节包含的例子中使用。更多细节请看 这里

你可以将 @Autowired 注解应用于构造函数,如下例所示。

Java

Kotlin

public class MovieRecommender {
​
    private final CustomerPreferenceDao customerPreferenceDao;
​
    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
​
    // ...
}
从Spring Framework 4.3开始,如果目标Bean一开始就只定义了一个构造函数,那么在这样的构造函数上就不再需要 @Autowired 注解。然而,如果有几个构造函数,而且没有主要/默认构造函数,那么至少有一个构造函数必须用 @Autowired 注解,以便指示容器使用哪一个。详情请参见关于 构造函数解析 的讨论。

你也可以将 @Autowired 注解应用于传统的setter方法,如下例所示。

Java

Kotlin

public class SimpleMovieLister {
​
    private MovieFinder movieFinder;
​
    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
​
    // ...
}

你也可以将注解应用于具有任意名称和多个参数的方法,如下例所示。

Java

Kotlin

public class MovieRecommender {
​
    private MovieCatalog movieCatalog;
​
    private CustomerPreferenceDao customerPreferenceDao;
​
    @Autowired
    public void prepare(MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }
​
    // ...
}

你也可以将 @Autowired 应用于字段,甚至将其与构造函数混合,如下例所示。

Java

Kotlin

public class MovieRecommender {
​
    private final CustomerPreferenceDao customerPreferenceDao;
​
    @Autowired
    private MovieCatalog movieCatalog;
​
    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
​
    // ...
}
确保你的目标组件(例如 MovieCatalogCustomerPreferenceDao)由你用于 @Autowired 注解的注入点的类型统一声明。否则,注入可能会在运行时由于 "no type match found" 的错误而失败。对于通过classpath扫描找到的XML定义的Bean或组件类,容器通常预先知道具体类型。然而,对于 @Bean 工厂方法,你需要确保声明的返回类型有足够的表现力。对于实现了多个接口的组件或可能被其实现类型引用的组件,考虑在你的工厂方法上声明最具体的返回类型(至少要与引用你的 bean 的注入点所要求的具体类型一样)。

你也可以指示Spring从 ApplicationContext 中提供所有特定类型的Bean,方法是将 @Autowired 注解添加到期望有该类型数组的字段或方法中,如下例所示。

Java

Kotlin

public class MovieRecommender {
​
    @Autowired
    private MovieCatalog[] movieCatalogs;
​
    // ...
}

这同样适用于类型化的(泛型)集合,正如下面的例子所示。

Java

Kotlin

public class MovieRecommender {
​
    private Set<MovieCatalog> movieCatalogs;
​
    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }
​
    // ...
}
你的目标Bean可以实现 org.springframework.core.Ordered 接口,如果你想让数组或列表中的项目以特定的顺序排序,可以使用 @Order 或标准的 @Priority 注解。否则,它们的顺序将遵循容器中相应目标Bean定义的注册顺序。你可以在目标类层面和 @Bean 方法上声明 @Order 注解,可能是针对单个Bean定义(在使用相同Bean类的多个定义的情况下)。@Order 值可以影响注入点的优先级,但要注意它们不会影响singleton的启动顺序,这是一个由依赖关系和 @DependsOn 声明决定的正交问题。请注意,标准的 jakarta.annotation.Priority 注解在 @Bean 级别上是不可用的,因为它不能被声明在方法上。它的语义可以通过 @Order 值与 @Primary 在每个类型的单例Bean上的组合来建模。

即使是类型化的 Map 实例也可以被自动注入,只要预期的key类型是 String。map的值包含所有预期类型的Bean,而key则包含相应的Bean名称,正如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

默认情况下,当一个给定的注入点没有匹配的候选Bean可用时,自动注入就会失败。在声明的数组、collection或map的情况下,预计至少有一个匹配的元素。

默认行为是将注解的方法和字段视为表示必须的依赖关系。你可以改变这种行为,就像下面的例子所展示的那样,通过将其标记为非必需(即通过将 @Autowired 中的 required 属性设置为 false),使框架能够跳过一个不可满足的注入点。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
如果一个非必须(required)的方法(或者在有多个参数的情况下,它的一个依赖关系)不可用,那么它将根本不会被调用。在这种情况下,一个非必须(required)字段将根本不会被填充,而是将其默认值留在原地。换句话说,将 required 属性设置为 false 表示相应的属性对于自动注入来说是可选的,如果该属性不能被自动注入,它将被忽略。这使得属性可以被分配默认值,这些默认值可以通过依赖注入选择性地被重写。

注入的构造函数和工厂方法参数是一种特殊情况,因为由于Spring的构造函数解析算法有可能处理多个构造函数,所以 @Autowired 中的 required 属性有一些不同的含义。构造函数和工厂方法参数实际上是默认需要的,但在单构造函数的情况下有一些特殊的规则,比如多元素注入点(数组、collection、map)如果没有匹配的Bean,则解析为空实例。这允许一种常见的实现模式,即所有的依赖关系都可以在一个独特的多参数构造函数中声明—例如,声明为一个没有 @Autowired 注解的单一公共构造函数。

任何给定的Bean类中只有一个构造函数可以声明 @Autowired,并将 required 属性设置为 true,表示该构造函数在用作Spring Bean时要自动注入。因此,如果 required 属性的默认值为 true,则只有一个构造函数可以使用 @Autowired 注解。如果有多个构造函数声明该注解,它们都必须声明 required=false,才能被视为自动注入的候选者(类似于XML中的 autowire=constructor)。具有最大数量的依赖关系的构造函数将被选中,这些依赖关系可以通过Spring容器中的匹配Bean来满足。如果没有一个候选者可以被满足,那么将使用一个主要的/默认的构造函数(如果存在的话)。同样地,如果一个类声明了多个构造函数,但没有一个是用 @Autowired 注解的,那么将使用一个主要/默认构造函数(如果存在的话)。如果一个类一开始只声明了一个构造函数,那么即使没有注解,它也会被使用。请注意,被注解的构造函数不一定是公共的(public)。

另外,你可以通过Java 8的 java.util.Optional 来表达特定依赖的非必须性质,正如下面的例子所示。

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

从Spring Framework 5.0开始,你也可以使用 @Nullable 注解(任何包中的任何类型—例如JSR-305中的 javax.annotation.Nullable),或者直接利用Kotlin内置的 null-safety 支持。

Java

Kotlin

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

你也可以对那些众所周知的可解析依赖的接口使用 @AutowiredBeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisherMessageSource。这些接口和它们的扩展接口,如 ConfigurableApplicationContextResourcePatternResolver,将被自动解析,不需要特别的设置。下面的例子是自动注入一个 ApplicationContext 对象。

Java

Kotlin

public class MovieRecommender {
​
    @Autowired
    private ApplicationContext context;
​
    public MovieRecommender() {
    }
​
    // ...
}
@Autowired@Inject@Value@Resource 注解是由Spring BeanPostProcessor 实现处理的。这意味着你不能在你自己的 BeanPostProcessorBeanFactoryPostProcessor 类型(如果有的话)中应用这些注解。这些类型必须通过使用XML或Spring @Bean 方法明确地 "注入"。
1.9.2. 用 @Primary 对基于注解的自动注入进行微调

因为按类型自动注入可能会导致多个候选者,所以经常需要对选择过程进行更多的控制。实现这一目标的方法之一是使用Spring的 @Primary 注解。@Primary 表示,当多个Bean是自动注入到一个单值(single value)依赖的候选者时,应该优先考虑一个特定的Bean。如果在候选者中正好有一个主要(primary)Bean存在,它就会成为自动注入的值。

考虑以下配置,它将 firstMovieCatalog 定义为主 MovieCatalog

Java

Kotlin

@Configuration
public class MovieConfiguration {
​
    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }
​
    @Bean
    public MovieCatalog secondMovieCatalog() { ... }
​
    // ...
}

通过前面的配置,下面的 MovieRecommender 被自动注入到 firstMovieCatalog

Java

Kotlin

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

相应的bean类定义如下。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
1.9.3. 用 Qualifiers 微调基于注解的自动注入

当可以确定一个主要的候选者时,@Primary 是按类型使用自动装配的一种有效方式,有几个实例。当你需要对选择过程进行更多控制时,你可以使用Spring的 @Qualifier 注解。你可以将限定符的值与特定的参数联系起来,缩小类型匹配的范围,从而为每个参数选择一个特定的bean。在最简单的情况下,这可以是一个普通的描述性值,如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

你也可以在单个构造函数参数或方法参数上指定 @Qualifier 注解,如以下例子所示。

Java

Kotlin

public class MovieRecommender {

    private final MovieCatalog movieCatalog;

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

下面的例子显示了相应的bean定义。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main"/> 

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> 

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
具有 main qualifier 值的bean与具有相同 qualifier 值的构造函数参数相注入
具有 action qualifier 值的bean与具有相同 qualifier 值的构造器参数相注入。

对于回退匹配(fallback match),Bean的名字被认为是默认的限定符值。因此,你可以用 mainid 来定义Bean,而不是嵌套的限定符元素,导致同样的匹配结果。然而,尽管你可以使用这个约定来引用特定的Bean的名字,但 @Autowired 从根本上说是关于类型驱动的注入,并带有可选的语义限定词。这意味着限定符的值,即使有Bean名称的回退,也总是在类型匹配的集合中具有缩小的语义。它们在语义上并不表达对唯一Bean id 的引用。好的限定符值是 mainEMEApersistent,表达了独立于Bean id 的特定组件的特征,在匿名Bean定义的情况下,如前面的例子中,它可能是自动生成的。

如前所述,qualifier 也适用于类型化(泛型)的集合—例如,适用于 Set。在这种情况下,所有匹配的bean,根据声明的限定词,被作为一个集合注入。这意味着限定词不一定是唯一的。相反,它们构成过滤标准。例如,你可以用相同的限定词值 "action" 来定义多个 MovieCatalog Bean,所有这些都被注入到一个用 @Qualifier("action") 注解的 Set 中。

在类型匹配候选者中,让 qualifier 值针对目标Bean名称进行选择,不需要在注入点上进行 @Qualifier 注解。如果没有其他解析指标(如qualifier或primary标记),对于非唯一的依赖情况,Spring会将注入点名称(即字段名或参数名)与目标Bean名称进行匹配,并选择同名的候选者(如果有)。

也就是说,如果你打算通过名字来表达注解驱动的注入,请不要主要使用 @Autowired,即使它能够在类型匹配的候选者中通过bean的名字来选择。相反,使用JSR-250的 @Resource 注解,它在语义上被定义为通过其唯一的名称来识别特定的目标组件,而声明的类型与匹配过程无关。@Autowired 具有相当不同的语义。在通过类型选择候选Bean后,指定的 String qualifier 值只在这些类型选择的候选中被考虑(例如,将 account qualifier 与标有相同 qualifier 标签的Bean匹配)。

对于那些本身被定义为集合、Map 或数组类型的Bean,@Resource 是一个很好的解决方案,它通过唯一的名称来引用特定的集合或数组Bean。也就是说,从4.3版本开始,你也可以通过Spring的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型,只要在 @Bean 返回类型签名或集合继承层次中保留元素类型信息。在这种情况下,你可以使用 qualifier 值在相同类型的集合中进行选择,如上一段所述。

从4.3版开始,@Autowired 也考虑到了用于注入的自我引用(也就是对当前注入的Bean的引用)。请注意,自我注入是一种回退(fallback)。对其他组件的常规依赖总是具有优先权。在这个意义上,自我引用不参与常规的候选选择,因此特别是永远不会是主要的(primary)。相反,他们总是以最低的优先级结束。在实践中,你应该把自引用作为最后的手段(例如,通过Bean的事务代理调用同一实例上的其他方法)。在这种情况下,可以考虑将受影响的方法分解到一个单独的委托Bean中。另外,你也可以使用 @Resource,它可以通过唯一的名字获得一个回到当前Bean的代理。

试图在同一个配置类上注入 @Bean 方法的结果,实际上也是一种自我引用的情况。要么在实际需要的地方(相对于配置类中的自动注入字段)的方法签名中延迟地解析这种引用,要么将受影响的 @Bean 方法声明为 static,将它们与包含的配置类实例及其生命周期解耦。否则,这些Bean只在 fallback 阶段被考虑,其他配置类上的匹配Bean将被选为主要(primary)候选者(如果有的话)。

@Autowired 适用于字段、构造函数和多参数方法,允许在参数级别上通过 qualifier 注解来缩小范围。相比之下,@Resource 只支持字段和只有一个参数的bean属性setter方法。因此,如果你的注入目标是构造函数或多参数方法,你应该坚持使用 qualifier。

你可以创建你自己的自定义 qualifier 注解。要做到这一点,请定义一个注解,并在你的定义中提供 @Qualifier 注解,如下面的例子所示。

Java

Kotlin

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

    String value();
}

然后你可以在自动注入的字段和参数上提供自定义 qualifier,如下例所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    @Genre("Action")
    private MovieCatalog actionCatalog;

    private MovieCatalog comedyCatalog;

    @Autowired
    public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
        this.comedyCatalog = comedyCatalog;
    }

    // ...
}

接下来,你可以提供候选Bean定义的信息。你可以添加 标签作为 标签的子元素,然后指定 typevalue 来匹配你的自定义qualifier注解。type是与注解的全限定类名相匹配的。另外,如果不存在名称冲突的风险,作为一种方便,你可以使用简短的类名。下面的例子演示了这两种方法。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

Classpath扫描和管理的组件 中,你可以看到一个基于注解的替代方案,以XML提供限定符元数据。具体来说,请看 用注解提供 Qualifier 元数据

在某些情况下,使用没有值的注解可能就足够了。当注解服务于一个更通用的目的并且可以应用于几个不同类型的依赖关系时,这可能是有用的。例如,你可以提供一个 offline 目录,在没有互联网连接的情况下可以进行搜索。首先,定义简单的注解,如下面的例子所示。

Java

Kotlin

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}

然后将注解添加到要自动注入的字段或属性中,如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    @Offline 
    private MovieCatalog offlineCatalog;

    // ...
}
这一行添加了 @Offline 注解。

现在,Bean定义只需要一个 qualifier type,如下面的例子所示。

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> 
    <!-- inject any dependencies required by this bean -->
</bean>
这个元素指定了 qualifier。

你也可以定义自定义的 qualifier 注解,除了简单的 value 属性之外,还接受命名的(named)属性,或者代替简单的值属性。如果在要自动注入的字段或参数上指定了多个属性值,那么Bean定义必须与所有这些属性值相匹配才能被认为是自动注入的候选者。作为一个例子,考虑下面的注解定义。

Java

Kotlin

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();

    Format format();
}

在这种情况下,Format 是一个枚举,定义如下。

Java

Kotlin

public enum Format {
    VHS, DVD, BLURAY
}

要自动注入的字段用自定义 qualifier 注解,并包括两个属性的值:genreformat,如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Action")
    private MovieCatalog actionVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Comedy")
    private MovieCatalog comedyVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.DVD, genre="Action")
    private MovieCatalog actionDvdCatalog;

    @Autowired
    @MovieQualifier(format=Format.BLURAY, genre="Comedy")
    private MovieCatalog comedyBluRayCatalog;

    // ...
}

最后,Bean的定义应该包含匹配的 qualifier 值。这个例子还展示了你可以使用Bean元属性来代替 元素。如果有的话, 元素及其属性优先,但如果没有这样的qualifier,自动注入机制就会回到 `` 标签中提供的值,就像下面例子中的最后两个Bean定义。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
​
    <context:annotation-config/>
​
    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>
​
    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>
​
    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>
​
    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>
​
</beans>
1.9.4. 使用泛型作为自动注入 Qualifier

除了 @Qualifier 注解外,你还可以使用Java泛型作为隐含的限定形式。例如,假设你有下面的配置。

Java

Kotlin

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

假设前面的Bean实现了一个泛型接口,(即 StoreStore),你可以 @Autowire Store 接口,泛型被用作qualifier,如下例所示。

Java

Kotlin

@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean

泛型 qualifier 也适用于自动注入list、Map 实例和数组。下面的例子是自动注入一个泛型 List

Java

Kotlin

// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;
1.9.5. 使用 CustomAutowireConfigurer

CustomAutowireConfigurer 是一个 BeanFactoryPostProcessor,可以让你注册自己的自定义 qualifier 注解类型,即使它们没有用Spring的 @Qualifier 注解来注解。下面的例子展示了如何使用 CustomAutowireConfigurer

<bean id="customAutowireConfigurer"
        class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>

AutowireCandidateResolver 通过以下方式确定自动注入的候选人。

  • 每个Bean定义的 autowire-candidate 值。

  • ` 元素上可用的任何默认的autowire-candidates` pattern。

  • 存在 @Qualifier 注解和任何用 CustomAutowireConfigurer 注册的自定义注解。

当多个Bean有资格成为自动注入的候选者时,“primary” 的确定方法如下。如果候选Bean定义中正好有一个 Primary 属性被设置为 true,它就被选中。

1.9.6. 用 @Resource 注入

Spring还支持通过在字段或Bean属性设置方法上使用JSR-250 @Resource 注解(jakarta.annotation.Resource)进行注入。这是Jakarta EE中的一种常见模式:例如,在JSF管理的Bean和JAX-WS端点。对于Spring管理的对象,Spring也支持这种模式。

@Resource 需要一个 name 属性。默认情况下,Spring将该值解释为要注入的Bean名称。换句话说,它遵循按名称的语义,正如下面的例子所展示的。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
这一行注入了一个 @Resource

如果没有明确指定名字,默认的名字来自于字段名或setter方法。如果是一个字段,它采用字段名。如果是setter方法,则采用Bean的属性名。下面的例子将把名为 movieFinder 的bean注入它的setter方法中。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
与注解一起提供的名称被 CommonAnnotationBeanPostProcessor 所知道的 ApplicationContext 解析为一个Bean名称。如果你明确地配置Spring的 SimpleJndiBeanFactory,这些名字可以通过JNDI解析。然而,我们建议你依靠默认行为,并使用Spring的JNDI查询功能来保留代理的级别。

在没有明确指定名称的 @Resource 使用的特殊情况下,与 @Autowired 类似,@Resource 找到一个主要的类型匹配,而不是一个特定的命名的 bean,并解析众所周知的可解析的依赖:BeanFactoryApplicationContextResourceLoaderApplicationEventPublisherMessageSource 接口。

因此,在下面的例子中,customerPreferenceDao 字段首先寻找名为 "customerPreferenceDao" 的Bean,然后回退到 CustomerPreferenceDao 类型的 primary 类型匹配。

Java

Kotlin

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; 

    public MovieRecommender() {
    }

    // ...
}
context 字段是根据已知的可解析依赖类型注入的:ApplicationContext
1.9.7. 使用 @Value

@Value 通常用于注入外部化properties。

Java

Kotlin

@Component
public class MovieRecommender {
​
    private final String catalog;
​
    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

采用以下配置。

Java

Kotlin

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

以及以下 application.properties 文件。

catalog.name=MovieCatalog

在这种情况下,catalog 参数和字段将等于 MovieCatalog 值。

Spring提供了一个默认的宽松的嵌入式值解析器(value resolver)。它将尝试解析属性值,如果无法解析,属性名称(例如 ${catalog.name})将被注入作为值。如果你想对不存在的值保持严格的控制,你应该声明一个 PropertySourcesPlaceholderConfigurer Bean,如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}
当使用 JavaConfig 配置 PropertySourcesPlaceholderConfigurer 时,@Bean 方法必须是 static 的。

使用上述配置可以确保在任何 ${} 占位符无法解析的情况下Spring初始化失败。也可以使用 setPlaceholderPrefixsetPlaceholderSuffixsetValueSeparator 等方法来定制占位符。

Spring Boot默认配置了一个 PropertySourcesPlaceholderConfigurer Bean,它将从 application.propertiesapplication.yml 文件中获取属性。

Spring提供的内置转换器支持允许自动处理简单的类型转换(例如转换为 Integerint)。多个逗号分隔的值可以自动转换为 String 数组,无需额外的操作。

可以提供一个默认值,如下所示。

Java

Kotlin

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
        this.catalog = catalog;
    }
}

Spring BeanPostProcessor 在幕后使用一个 ConversionService 来处理将 @Value 中的 String 值转换为目标类型的过程。如果你想为你自己的自定义类型提供转换支持,你可以提供你自己的 ConversionService Bean实例,如下例所示。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}

@Value 包含一个 SpEL 表达式 时,该值将在运行时被动态计算出来,如下例所示。

Java

Kotlin

@Component
public class MovieRecommender {
​
    private final String catalog;
​
    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}

SpEL还能够使用更复杂的数据结构。

Java

Kotlin

@Component
public class MovieRecommender {
​
    private final Map<String, Integer> countOfMoviesPerCatalog;
​
    public MovieRecommender(
            @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}
1.9.8. 使用 @PostConstruct@PreDestroy

CommonAnnotationBeanPostProcessor 不仅可以识别 @Resource 注解,还可以识别JSR-250生命周期注解:jakarta.annotation.PostConstructjakarta.annotation.PreDestroy。在Spring 2.5中引入,对这些注解的支持为 初始化回调销毁回调中描述的生命周期回调机制提供了一个替代方案。只要在Spring ApplicationContext 中注册了 CommonAnnotationBeanPostProcessor,携带这些注解之一的方法就会在生命周期中与相应的Spring生命周期接口方法或明确声明的回调方法在同一时间被调用。在下面的例子中,缓存在初始化时被预先填充,在销毁时被清除。

Java

Kotlin

public class CachingMovieLister {
​
    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }
​
    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}

关于结合各种生命周期机制的效果,详见 结合生命周期机制

@Resource 一样,@PostConstruct@PreDestroy 注解类型在JDK 6到8中是标准Java库的一部分。然而,整个 javax.annotation 包在JDK 9中从核心Java模块中分离出来,最终在JDK 11中被删除。从Jakarta EE 9开始,该包现在住在 jakarta.annotation 中。如果需要,现在需要通过Maven中心获得 jakarta.annotation-api 工件,只需像其他库一样添加到应用程序的classpath中即可。
  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值