代码示例

本文随附的工作代码示例可在 GitHub 仓库 中找到。

为什么我要在启动时执行代码?

在应用程序启动时执行逻辑的最关键用例是:确保应用程序仅在完成所有必要设置、能够支持后续处理时,才开始处理数据。

假设我们的应用程序是事件驱动的,它从队列中提取事件,处理后再将新事件发送到另一个队列。在这种情况下,我们希望应用程序仅在与目标队列的连接准备就绪、可以接收事件时,才开始从源队列提取事件。因此,我们需要包含一些启动逻辑,一旦目标连接准备就绪,便激活事件处理。

在更常规的设置中,应用程序响应 HTTP 请求,从数据库加载数据并将结果存回。我们只希望在数据库连接准备好之后才开始响应 HTTP 请求;否则,在连接就绪之前,我们将被迫返回 HTTP 500 错误。

Spring Boot 会自动处理许多此类情况,并且仅在应用程序完全“热启动”(Hot)后才会激活某些连接。

但是,对于自定义方案,我们需要一种使用自定义代码对应用程序启动做出反应的方法。Spring 和 Spring Boot 提供了几种机制,让我们依次查看它们。

CommandLineRunner

CommandLineRunner 是一个简单的接口,允许我们在 Spring 应用程序成功启动后执行代码:

@Component
@Order(1)
class MyCommandLineRunner implements CommandLineRunner {

  private static final Logger logger = ...;

  @Override
  public void run(String... args) throws Exception {
    if (args.length > 0) {
      logger.info("first command-line parameter: '{}'", args[0]);
    }
  }
}

当 Spring Boot 在应用程序上下文中找到一个 CommandLineRunner Bean 时,它会在应用程序启动后调用其 run() 方法,并传递用于启动应用程序的命令行参数。

现在,我们可以使用如下命令行参数启动应用程序:

java -jar application.jar --foo=bar

这将产生以下日志输出:

first command-line parameter: '--foo=bar'

如我们所见,该参数未被解析,而是被解释为带有值的单个参数 --foo=bar。稍后我们将看到 ApplicationRunner 如何为我们解析这些参数。

请注意 run() 签名中的 Exception。即使在某些案例中我们不会抛出异常,也不需要将其添加到签名中,但它的存在表明 Spring Boot 将在我们的 CommandLineRunner 中处理异常。Spring Boot 将 CommandLineRunner 视为应用程序启动的一部分,当它抛出异常时将中止启动

可以使用 @Order 注解将多个 CommandLineRunner 排列在一起。

当我们需要访问简单的、以空格分隔的命令行参数时,CommandLineRunner 是一种合适的方法。

不要过度使用 @Order

尽管 @Order 注解非常方便,可以将某些启动逻辑片段放入序列中,但这也表明这些启动片段彼此依赖。我们应该努力使依赖关系尽可能少,以创建可维护的代码库。

而且,@Order 注解创建了一种难以理解的逻辑依赖性,而不是易于捕获的编译时依赖性。将来,您可能会想知道该 @Order 注解的含义并删除它,从而导致难以排查的启动错误。

ApplicationRunner

如果我们需要解析命令行参数,可以改用 ApplicationRunner

@Component
@Order(2)
class MyApplicationRunner implements ApplicationRunner {

  private static final Logger logger = ...;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    logger.info("ApplicationRunner#run()");
    logger.info("foo: {}", args.getOptionValues("foo"));
  }
}

ApplicationArguments 对象使我们可以访问已解析的命令行参数。每个参数可以具有多个值,因为它们可能在命令行中多次使用。我们可以通过调用 getOptionValues() 获取特定参数值的数组。

让我们再次使用 foo 参数启动应用程序:

java -jar application.jar --foo=bar

结果日志输出如下所示:

foo: [bar]

CommandLineRunner 一样,run() 方法中的异常将中止应用程序启动,并且 ApplicationRunner 可以使用 @Order 注解按顺序放置多个实例。由 @Order 创建的序列在 CommandLineRunnerApplicationRunner 之间是共享的。

如果需要创建一些可以访问复杂命令行参数的全局启动逻辑,我们将使用 ApplicationRunner

ApplicationListener

如果我们不需要访问命令行参数,可以将启动逻辑绑定到 Spring 的 ApplicationReadyEvent

@Component
@Order(0)
class MyApplicationListener 
    implements ApplicationListener<ApplicationReadyEvent> {

  private static final Logger logger = ...;

  @Override
  public void onApplicationEvent(ApplicationReadyEvent event) {
    logger.info("ApplicationListener#onApplicationEvent()");
  }
}

ApplicationReadyEvent 仅在应用程序准备就绪后触发(顾名思义),这意味着上述监听器将在本文中描述的所有其他解决方案执行完毕后才工作

多个 ApplicationListener 可置于同一个顺序,使用 @Order 标注。订单序列仅与其他 ApplicationListener 共享,而不与 ApplicationRunnerCommandLineRunner 共享。

如果我们需要在不访问命令行参数的情况下创建一些全局启动逻辑,监听 ApplicationReadyEventApplicationListener 是首选方式。 我们仍然可以通过使用 Spring Boot 对 配置属性 的支持注入环境参数来访问它们。

@PostConstruct

创建启动逻辑的另一种简单解决方案是提供一种在 Bean 创建期间由 Spring 调用的初始化方法。我们要做的就是将 @PostConstruct 注解添加到方法中:

@Component
@DependsOn("myApplicationListener")
class MyPostConstructBean {

  private static final Logger logger = ...;

  @PostConstruct
  void postConstruct() {
    logger.info("@PostConstruct");
  }
}

一旦 MyPostConstructBean 类型的 Bean 成功实例化,Spring 就会调用此方法。

@PostConstruct 方法是在 Spring 创建 Bean 之后立即调用的,因此我们无法通过 @Order 注解自由地对其进行排序,因为它可能依赖于我们 Bean 中其他 @Autowired 的 Spring Bean。

相反,它将在依赖于它的所有 Bean 被初始化之后被调用。如果要添加人为的依赖关系并由此创建订单,则可以使用 @DependsOn 注解(与 @Order 注解同样的警告!)。

@PostConstruct 方法固有地依赖于特定的 Spring Bean,所以应该只用于该单个 Bean 的初始化逻辑。

对于全局初始化逻辑,CommandLineRunnerApplicationRunnerApplicationListener 提供了更好的解决方案。

InitializingBean

在效果上与 @PostConstruct 解决方案非常相似,我们可以实现 InitializingBean 接口并让 Spring 调用某种初始化方法:

@Component
class MyInitializingBean implements InitializingBean {

  private static final Logger logger = ...;

  @Override
  public void afterPropertiesSet() throws Exception {
    logger.info("InitializingBean#afterPropertiesSet()");
  }
}

Spring 将在应用程序启动期间调用 afterPropertiesSet() 方法。顾名思义,我们可以确保 Spring 填充了 Bean 的所有属性。如果我们在某些属性上使用 @Autowired(我们不应该——应该使用 构造函数注入),那么 Spring 将在调用 afterPropertiesSet() 之前将 Bean 注入到这些属性中,这与 @PostConstruct 相同。

对于 InitializingBean@PostConstruct,我们必须小心不要依赖于在另一个 Bean 的 afterPropertiesSet()@PostConstruct 方法中已初始化的状态。该状态可能尚未初始化,从而导致 NullPointerException

如果可能的话,我们应该使用 构造函数注入 并在构造函数中初始化所需的所有内容,因为这使此类错误成为不可能。

结论

在 Spring Boot 应用程序启动期间,有多种执行代码的方法。尽管它们看上去很相似,但是每个人的行为略有不同或提供不同的功能,因此它们都有存在的意义。

我们可以通过 @Order 注解影响不同启动 Bean 的顺序,但只能将其作为最后的手段,因为它在这些 Bean 之间引入了难以掌握的逻辑依赖性。

如果您想查看所有正在使用的解决方案,请查看 GitHub 仓库

说明:本文所述机制适用于 Spring Boot 2.x 及 3.x 版本。随着框架演进,部分接口(如 InitializingBean)虽仍可用但已非首选,建议优先使用构造函数注入与 @PostConstruct 或启动运行器。