自定义 Bean 的性质

TODAY Framework 提供了许多接口,您可以使用这些接口自定义 bean 的性质。本节将它们分组如下:

生命周期回调

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

JSR-250 @PostConstruct@PreDestroy 注解通常被认为是现代 Infra 应用程序中接收生命周期回调的最佳实践。 使用这些注解意味着您的 bean 不会耦合到特定于 Infra 的接口。 有关详细信息,请参阅 使用 @PostConstruct@PreDestroy

如果您不想使用 JSR-250 注解,但仍想消除耦合,请考虑使用 init-methoddestroy-method bean 定义元数据。

在内部,TODAY Framework 使用 BeanPostProcessor 实现来处理它能找到的任何回调接口并调用适当的方法。 如果您需要自定义特性或 Infra 默认未提供的其他生命周期行为,您可以自己实现一个 BeanPostProcessor。 有关更多信息,请参阅 容器扩展点

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

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

初始化回调

infra.beans.factory.InitializingBean 接口允许 bean 在容器设置了 bean 上所有必要的属性后执行初始化工作。 InitializingBean 接口指定了一个方法:

void afterPropertiesSet() throws Exception;

我们建议您不要使用 InitializingBean 接口,因为它不必要地将代码耦合到 Infra。 或者,我们建议使用 @PostConstruct 注解或指定 POJO 初始化方法。 在基于 XML 的配置元数据的情况下,您可以使用 init-method 属性来指定具有无参数签名的方法名称。 对于 Java 配置,您可以使用 @BeaninitMethod 属性。 请参阅 接收生命周期回调。考虑以下示例:

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

public class ExampleBean {

  public void init() {
    // 执行一些初始化工作
  }
}

前面的示例与下面的示例(由两个列表组成)几乎具有相同的效果:

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

public class AnotherExampleBean implements InitializingBean {

  @Override
  public void afterPropertiesSet() {
    // 执行一些初始化工作
  }
}

然而,前面的两个示例中的第一个并没有将代码耦合到 Infra。

请注意,@PostConstruct 和一般的初始化方法是在容器的单例创建锁内执行的。 只有在从 @PostConstruct 方法返回后,bean 实例才被视为完全初始化并准备好发布给其他人。 此类单独的初始化方法仅用于验证配置状态,并可能根据给定的配置准备一些数据结构,但不能进行进一步的外部 bean 访问活动。 否则,存在初始化死锁的风险。

对于需要触发昂贵的后初始化活动的场景,例如异步数据库准备步骤,您的 bean 应该实现 SmartInitializingSingleton.afterSingletonsInstantiated() 或依赖于上下文刷新事件: 实现 ApplicationListener<ContextRefreshedEvent> 或声明其注解等效项 @EventListener(ContextRefreshedEvent.class)。 这些变体出现在所有常规单例初始化之后,因此在任何单例创建锁之外。

或者,您可以实现 (Smart)Lifecycle 接口并与容器的整体生命周期管理集成,包括自动启动机制、预销毁停止步骤以及潜在的停止/重启回调(见下文)。

销毁回调

实现 infra.beans.factory.DisposableBean 接口允许 bean 在包含它的容器被销毁时获得回调。 DisposableBean 接口指定了一个方法:

void destroy() throws Exception;

我们建议您不要使用 DisposableBean 回调接口,因为它不必要地将代码耦合到 Infra。 或者,我们建议使用 @PreDestroy 注解或指定 bean 定义支持的通用方法。 对于基于 XML 的配置元数据,您可以使用 <bean/> 上的 destroy-method 属性。 对于 Java 配置,您可以使用 @BeandestroyMethod 属性。 请参阅 接收生命周期回调。考虑以下定义:

<bean id="exampleDestructionBean" class="examples.ExampleBean" destroy-method="cleanup"/>
  • Java

public class ExampleBean {

  public void cleanup() {
    // 执行一些销毁工作(如释放池化连接)
  }
}

前面的定义与下面的定义几乎具有相同的效果:

<bean id="exampleDestructionBean" class="examples.AnotherExampleBean"/>
  • Java

public class AnotherExampleBean implements DisposableBean {

  @Override
  public void destroy() {
    // 执行一些销毁工作(如释放池化连接)
  }
}

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

请注意,Infra 还支持销毁方法的推断,即检测公共的 closeshutdown 方法。 这是 Java 配置类中 @Bean 方法的默认行为,并自动匹配 java.lang.AutoCloseablejava.io.Closeable 实现, 也不会将销毁逻辑耦合到 Infra。

对于 XML 的销毁方法推断,您可以将 <bean> 元素的 destroy-method 属性分配一个特殊的 (inferred) 值, 这指示 Infra 自动检测特定 bean 定义的 bean 类上的公共 closeshutdown 方法。 您还可以在 <beans> 元素的 default-destroy-method 属性上设置此特殊 (inferred) 值, 以将此行为应用于整个 bean 定义集(参见 默认初始化和销毁方法)。

对于扩展的关闭阶段,您可以实现 Lifecycle 接口,并在任何单例 bean 的销毁方法被调用之前接收提前停止信号。 您还可以实现 SmartLifecycle 以进行有时间限制的停止步骤,其中容器将等待所有此类停止处理完成后再继续进行销毁方法。

默认初始化和销毁方法

当您编写不使用 Infra 特定的 InitializingBeanDisposableBean 回调接口的初始化和销毁方法回调时, 您通常会编写名为 init()initialize()dispose() 等的方法。 理想情况下,此类生命周期回调方法的名称应在整个项目中标准化,以便所有开发人员使用相同的方法名称并确保一致性。

您可以配置 Infra 容器以在每个 bean 上“查找”命名的初始化和销毁回调方法名称。 这意味着,作为应用程序开发人员,您可以编写应用程序类并使用名为 init() 的初始化回调, 而无需为每个 bean 定义配置 init-method="init" 属性。 Infra IoC 容器在创建 bean 时调用该方法(并遵循 前面描述的 标准生命周期回调契约)。 此功能还强制执行初始化和销毁方法回调的一致命名约定。

假设您的初始化回调方法名为 init(),销毁回调方法名为 destroy()。 您的类类似于以下示例中的类:

  • Java

public class DefaultBlogService implements BlogService {

  private BlogDao blogDao;

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

  // 这(不出所料)是初始化回调方法
  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>

顶层 <beans/> 元素属性上 default-init-method 属性的存在会导致 Infra IoC 容器将 bean 类上名为 init 的方法识别为初始化方法回调。 当 bean 被创建和组装时,如果 bean 类有这样的方法,它将在适当的时间被调用。

您可以通过使用顶层 <beans/> 元素上的 default-destroy-method 属性类似地配置销毁方法回调(即在 XML 中)。

如果现有的 bean 类已经具有与约定名称不同的回调方法,您可以通过使用 <bean/> 本身的 init-methoddestroy-method 属性指定(即在 XML 中)方法名称来覆盖默认值。

Infra 容器保证在为 bean 提供了所有依赖项后立即调用配置的初始化回调。 因此,初始化回调是在原始 bean 引用上调用的,这意味着 AOP 拦截器等尚未应用于 bean。 首先完全创建目标 bean,然后应用(例如)带有拦截器链的 AOP 代理。 如果目标 bean 和代理是分开定义的,您的代码甚至可以与原始目标 bean 交互,绕过代理。 因此,将拦截器应用于 init 方法是不一致的,因为这样做会将目标 bean 的生命周期耦合到其代理或拦截器, 并在您的代码直接与原始目标 bean 交互时留下奇怪的语义。

组合生命周期机制

您有三个选项来控制 bean 生命周期行为:

您可以组合这些机制来控制给定的 bean。

如果为 bean 配置了多种生命周期机制,并且每种机制都配置了不同的方法名称,则每个配置的方法都将按照本说明后的顺序列出运行。 但是,如果为多种生命周期机制配置了相同的方法名称——例如,初始化方法的 init()——则该方法只运行一次, 如 上一节 所述。

为同一个 bean 配置的多种生命周期机制,具有不同的初始化方法,调用顺序如下:

  1. 带有 @PostConstruct 注解的方法

  2. InitializingBean 回调接口定义的 afterPropertiesSet()

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

销毁方法的调用顺序相同:

  1. 带有 @PreDestroy 注解的方法

  2. DisposableBean 回调接口定义的 destroy()

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

启动和关闭回调

Lifecycle 接口为任何具有自身生命周期需求的对象(如启动和停止某些后台进程)定义了基本方法:

public interface Lifecycle {

  void start();

  void stop();

  boolean isRunning();
}

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

public interface LifecycleProcessor extends Lifecycle {

  void onRefresh();

  void onClose();
}

请注意,LifecycleProcessor 本身是 Lifecycle 接口的扩展。 它还添加了另外两个方法,用于响应上下文被刷新和关闭。

请注意,常规的 infra.context.Lifecycle 接口是一个用于显式启动和停止通知的普通契约,并不意味着在上下文刷新时自动启动。 对于对自动启动的细粒度控制以及特定 bean 的优雅停止(包括启动和停止阶段),请考虑实现扩展的 infra.context.SmartLifecycle 接口。

此外,请注意,不能保证停止通知在销毁之前到来。 在常规关闭时,所有 Lifecycle bean 在传播一般销毁回调之前首先接收停止通知。 但是,在上下文生命周期内的热刷新或停止的刷新尝试期间,仅调用销毁方法。

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

public interface Phased {

  int getPhase();
}

下面的列表显示了 SmartLifecycle 接口的定义:

public interface SmartLifecycle extends Lifecycle, Phased {

  boolean isAutoStartup();

  void stop(Runnable callback);
}

启动时,具有最低相位的对象首先启动。停止时,遵循相反的顺序。 因此,实现 SmartLifecycle 且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将是首先启动和最后停止的对象之一。 在频谱的另一端,相位值 Integer.MAX_VALUE 将指示该对象应最后启动并首先停止(可能是因为它依赖于其他正在运行的进程)。 在考虑相位值时,了解未实现 SmartLifecycle 的任何“正常”Lifecycle 对象的默认相位为 0 也很重要。 因此,任何负相位值表示对象应在这些标准组件之前启动(并在它们之后停止)。任何正相位值则相反。

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

<bean id="lifecycleProcessor" class="infra.context.support.DefaultLifecycleProcessor">
  <!-- 超时值,单位毫秒 -->
  <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

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

在非 Web 应用程序中优雅地关闭 Infra IoC 容器

本节仅适用于非 Web 应用程序。基于 Infra Web 的 ApplicationContext 实现已经具备了在相关 Web 应用程序关闭时优雅地关闭 Infra IoC 容器的代码。

如果您在非 Web 应用程序环境(例如,富客户端桌面环境)中使用 Infra IoC 容器,请向 JVM 注册关闭钩子。 这样做可以确保优雅关闭,并在您的单例 bean 上调用相关的销毁方法,以便释放所有资源。 您仍然必须正确配置和实现这些销毁回调。

要注册关闭钩子,请调用 ConfigurableApplicationContext 接口上声明的 registerShutdownHook() 方法,如下例所示:

  • Java

import infra.context.ConfigurableApplicationContext;
import infra.context.support.ClassPathXmlApplicationContext;

public final class Boot {

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

    // 为上述上下文添加关闭钩子...
    ctx.registerShutdownHook();

    // 应用程序在此处运行...

    // main 方法退出,钩子在应用程序关闭之前被调用...
  }
}

线程安全性和可见性

Infra 核心容器以线程安全的方式发布创建的单例实例,通过单例锁保护访问并保证在其他线程中的可见性。

因此,应用程序提供的 bean 类不必担心其初始化状态的可见性。 常规配置字段只要仅在初始化阶段发生变化,就不必标记为 volatile,即使对于在该初始阶段可变的基于 setter 的配置状态,也提供类似于 final 的可见性保证。 如果此类字段在 bean 创建阶段及其随后的初始发布之后发生更改,则需要在访问时声明为 volatile 或由公共锁保护。

请注意,对此类单例 bean 实例中的配置状态的并发访问(例如,对于控制器实例或存储库实例),在从容器侧进行此类安全初始发布后是完全线程安全的。 这包括常见的单例 FactoryBean 实例,它们也在通用单例锁内处理。

对于销毁回调,配置状态保持线程安全,但在初始化和销毁之间累积的任何运行时状态应根据通用 Java 指南保存在线程安全结构中(或对于简单情况保存在 volatile 字段中)。

如上所示的更深层次的 Lifecycle 集成涉及运行时可变状态,例如 runnable 字段,该字段必须声明为 volatile。 虽然常见的生命周期回调遵循一定的顺序,例如启动回调保证仅在完全初始化后发生,停止回调仅在初始启动后发生, 但有一个特殊情况是销毁前的常见停止安排:强烈建议任何此类 bean 中的内部状态也允许在没有先前停止的情况下立即进行销毁回调, 因为这可能在取消引导后的非凡关闭期间发生,或者在由另一个 bean 引起的停止超时的情况下发生。

ApplicationContextAware and BeanNameAware

ApplicationContext 创建一个实现 infra.context.ApplicationContextAware 接口的对象实例时,该实例将获得对该 ApplicationContext 的引用。 下面的列表显示了 ApplicationContextAware 接口的定义:

public interface ApplicationContextAware {

  void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,beans 可以通过 ApplicationContext 接口或通过将引用转换为该接口的已知子类(例如 ConfigurableApplicationContext,它公开了附加功能), 以编程方式操作创建它们的 ApplicationContext。 一种用途是以编程方式检索其他 beans。有时这种能力很有用。 但是,一般来说,您应该避免这样做,因为它将代码耦合到 Infra,并且不遵循控制反转风格,在控制反转风格中,协作者作为属性提供给 beans。 ApplicationContext 的其他方法提供对文件资源的访问、发布应用程序事件以及访问 MessageSource。 这些附加功能在 ApplicationContext 的附加功能 中描述。

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

ApplicationContext 创建一个实现 infra.beans.factory.BeanNameAware 接口的类时,该类将获得对其关联对象定义中定义的名称的引用。 下面的列表显示了 BeanNameAware 接口的定义:

public interface BeanNameAware {

  void setBeanName(String name) throws BeansException;
}

该回调在填充正常 bean 属性之后,但在初始化回调(如 InitializingBean.afterPropertiesSet() 或自定义 init-method)之前调用。

其他 Aware 接口

除了 ApplicationContextAwareBeanNameAware前面 讨论过), Infra 提供了各种 Aware 回调接口,允许 beans 向容器指示它们需要某种基础设施依赖项。 作为一般规则,名称指示依赖项类型。下表总结了最重要的 Aware 接口:

Table 1. Aware 接口
名称 注入的依赖项 解释在…​

ApplicationContextAware

声明 ApplicationContext

ApplicationContextAwareBeanNameAware

ApplicationEventPublisherAware

封闭 ApplicationContext 的事件发布者。

ApplicationContext 的附加功能

BeanClassLoaderAware

用于加载 bean 类的类加载器。

实例化 Beans

BeanFactoryAware

声明 BeanFactory

BeanFactory API

BeanNameAware

声明 bean 的名称。

ApplicationContextAwareBeanNameAware

LoadTimeWeaverAware

定义的用于在加载时处理类定义的 weaver。

TODAY Framework 中使用 AspectJ 进行加载时织入

MessageSourceAware

配置的消息解析策略(支持参数化和国际化)。

ApplicationContext 的附加功能

NotificationPublisherAware

Infra JMX 通知发布者。

通知

ResourceLoaderAware

配置的用于低级访问资源的加载器。

资源

再次注意,使用这些接口将您的代码绑定到 Infra API,并且不遵循控制反转风格。 因此,我们建议将它们用于需要以编程方式访问容器的基础设施 beans。