Ohhnews

分类导航

$ cd ..
Baeldung原文

Spring应用启动跟踪详解

#spring#应用启动#applicationstartup#启动跟踪#性能优化

Spring 应用启动追踪

1. 引言

应用启动的目标是提高加载后的整体性能。这意味着要创建所有初始使用所需的对象,同时还要缓存或准备极有可能需要的数据。除了改善用户体验外,应用启动本身也可能成为问题。

在本教程中,我们将学习如何促进、追踪和监控 Spring 应用的上下文创建。

首先,我们将回顾 Spring 上下文是什么以及为何要追踪其创建。之后,我们会了解 Spring Boot 提供的三种实现,用于轻松监控上下文创建。最后,我们快速探讨如何以及为何要实现自定义的启动追踪逻辑。

2. Spring 应用上下文

Spring Boot 应用上下文充当控制反转(IoC)容器。该容器管理 Spring 应用的生命周期,并提供依赖注入、Spring Bean 管理、事件和配置管理。

上下文在 Spring 应用启动时创建。对于简单应用,Spring 应用通常会快速启动,但对于复杂应用可能需要更长时间,甚至在初始化期间冻结。

有多种方法可以提升启动速度为了帮助我们更好地理解启动过程,Spring 提供了追踪应用上下文创建的能力。

3. ApplicationStartup 接口

Spring 提供了 ApplicationStartup 接口的两种实现:

  • DefaultApplicationStartup
  • FlightRecorderApplicationStartup

默认实现是空操作,而 FlightRecorderApplicationStartup 提供了与 Java Flight Recorder (JFR) 配合使用的实现。

此外,Spring Boot 还提供了 BufferingApplicationStartup 实现,它将启动事件记录在内存中。如果我们想使用 DefaultApplicationStartup 之外的任何实现,则需要在启动应用之前使用所需的 ApplicationStartup 实现来初始化 Spring 应用。

4. BufferingApplicationStartup

我们来看一下 BufferingApplicationStartup,这是一个将启动事件存储在内存中,并提供一个 Actuator 端点以便于访问记录事件的实现。

4.1. 配置

首先,我们需要将 BufferingApplicationStartup 注册到 Spring Boot 应用中:

$ java
@SpringBootApplication
public class ApplicationStartupTrackingApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(ApplicationStartupTrackingApplication.class);
        app.setApplicationStartup(new BufferingApplicationStartup(2048));
        app.run(args);
    }
}

构造参数(此处为 2048)指定了缓冲区大小,即可以记录的最大事件数。

4.2. 通过 Actuator 端点访问

检索记录事件的一种方式是通过 Actuator 端点。具体来说,我们可以在应用属性中启用 startup actuator

$ config
management.endpoints.web.exposure.include=startup

如果我们有 Spring Bean,还需要进行必要的添加:

$ java
@Service
public class SpecialService {
}

访问 Actuator 时,可以看到该服务 Bean 的条目:

$ bash
curl localhost:8080/actuator/startup | jq
{
  "duration": "PT0.000754S",
  "endTime": "2026-07-19T18:31:27.493778Z",
  "startTime": "2026-07-19T18:31:27.493024Z",
  "startupStep": {
    "id": 107,
    "name": "spring.beans.instantiate",
    "parentId": 4,
    "tags": [
      {
        "key": "beanName",
        "value": "specialService"
      }
    ]
  }
}

需要注意的是,Actuator 仅适用于 BufferingApplicationStartup。如果我们使用 ApplicationStartup 接口的任何其他实现来配置应用,则会看到错误信息:

$ bash
curl localhost:8080/actuator/startup | jq
{
  "timestamp": "2026-07-19T14:37:45.989Z",
  "status": 404,
  "error": "Not Found",
  "path": "/actuator/startup"
}

此外,我们可以使用 GET 或 POST 请求调用 Actuator 端点。两种情况下响应相同;然而,GET 返回时间线的快照,而 POST 返回快照并清除缓冲区。

4.3. 通过应用上下文访问

除了使用 Actuator 端点,我们还可以以编程方式访问 BufferingApplicationStartup 提供的时间线。为此,我们需要访问已配置的 BufferingApplicationStartup 实例:

$ java
@Component
public class StartupTracker {
    private BufferingApplicationStartup startup;
    
    public StartupTracker(@Autowired ApplicationContext context) {
        startup = (BufferingApplicationStartup) (
            (ConfigurableApplicationContext) context).getApplicationStartup();
    }
    public List recorded() {
        List recordedEvents = new ArrayList<>();
        for(StartupTimeline.TimelineEvent event : startup.getBufferedTimeline().getEvents()) {
            for(StartupStep.Tag tag : event.getStartupStep().getTags()) {
                recordedEvents.add(
                    event.getStartupStep().getName() + "  " +
                        tag.getKey() + " " +
                        tag.getValue()
                );
            }
        }
        return recordedEvents;
    }
}

这样,我们就能访问更深入的启动上下文。

4.4. 在 Spring Boot 测试中使用

通过这种方式,我们可以分析时间线,创建自定义输出格式,过滤某些事件,还可以测试实现:

$ java
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
public class ApplicationTest {
    @Autowired
    private StartupTracker startupTracker;
    @Test
    void givenTheApplicationStarts_whenRetrieveRecordedEvents_ThenContainsCustomBeans() {
        Assertions.assertThat(startupTracker.recorded()).contains(
            "spring.beans.instantiate  beanName specialService",
            "spring.beans.instantiate  beanName startupTracker"
        );
    }
}

可以看到,事件列表包含我们创建的两个 Bean:SpecialServiceStartupTracker

值得注意的是,我们需要使用 SpringBootTest.UseMainMethod.ALWAYS 配置测试。这确保了通过 main 方法启动应用,并且 Spring Boot 被配置为使用 BufferingApplicationStartup 而不是空操作实现。

5. FlightRecorderApplicationStartup

另一个应用启动追踪选项是 FlightRecorderApplicationStartup这是一种实现,它提供适合 Java Flight Recorder (JFR) 的 Spring 启动指标,可以在 Java Mission Control (JMC) 工具中查看

现在配置应用使用 FlightRecorderApplicationStartup

$ java
app.setApplicationStartup(new FlightRecorderApplicationStartup());

此外,我们还需要提供一个 JVM 参数:

$ bash
-XX:StartFlightRecording:filename=recording.jfr,duration=20s

现在,Spring 会将应用启动事件记录到一个名为 recording.jfr 的文件中,我们可以在 Java Mission Control (JMC) 查看器中查看: [LOADING...]

特别是,我们可以在 Spring Application 类别下找到启动步骤。

6. 添加自定义步骤

向启动时间线添加自定义步骤的推荐方式是将自定义启动步骤添加到 ApplicationStartup 实例中,该实例我们可以从应用上下文获取。为了演示如何实现,让我们创建一个服务类并添加一些自定义步骤:

$ java
@Service
public class SpecialService {
    private ApplicationStartup applicationStartup;
    public SpecialService(ApplicationContext context) {
        this.applicationStartup = ((ConfigurableApplicationContext) context).getApplicationStartup();
    }
    @PostConstruct
    public void init() {
        StartupStep startupStep1 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep1.tag("init", "connect to databases");
            // 一些长时间运行的初始化
        } finally {
            startupStep1.end();
        }
        StartupStep startupStep2 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep2.tag("init", "connect to AI agent");
            // 更多长时间运行的初始化
        } finally {
            startupStep2.end();
        }
    }
}

因此,我们现在可以在 Actuator 的响应中看到 SpecialService bean 初始化的步骤作为父步骤(id=107)以及两个额外步骤(id=[108,109]):

$ cat
[
  {
    "duration": "PT0.000006S",
    "endTime": "2026-07-18T19:15:12.096227Z",
    "startTime": "2026-07-18T19:15:12.096221Z",
    "startupStep": {
      "id": 108,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to databases"
        }
      ]
    }
  },
  {
    "duration": "PT0.000002S",
    "endTime": "2026-07-18T19:15:12.096238Z",
    "startTime": "2026-07-18T19:15:12.096236Z",
    "startupStep": {
      "id": 109,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to AI agent"
        }
      ]
    }
  },
  {
    "duration": "PT0.000682S",
    "endTime": "2026-07-18T19:15:12.096328Z",
    "startTime": "2026-07-18T19:15:12.095646Z",
    "startupStep": {
      "id": 107,
      "name": "spring.beans.instantiate",
      "parentId": 4,
      "tags": [
        {
          "key": "beanName",
          "value": "specialService"
        }
      ]
    }
  }
]

输出显示了两个启动步骤的指标:connect to databasesconnect to AI agent,以及服务本身的创建步骤 spring.beans.instantiate。如前所述,我们也可以在 Java Flight Recorder 中或以编程方式通过 BufferingApplicationStartup 监控这些启动步骤。

7. 实现 ApplicationStartup 接口

访问 Spring 启动指标的推荐方式是使用 Spring 提供的 ApplicationStartup 接口的三种实现之一。不过,也可以提供自定义实现:

$ java
public class CustomStartup implements ApplicationStartup {
    @Override
    public StartupStep start(String name) {
        return new CustomStartupStep(name);
    }
}

在这种情况下,我们还应该提供 StartupStep 接口的实现:

$ java
public class CustomStartupStep implements StartupStep {
    private String name;
    public CustomStartupStep(String name) {
        this.name = name;
    }
    @Override
    public String getName() {
        return name;
    }
    // 覆盖其他方法
}

由于这是一种罕见场景,我们不讨论自定义 ApplicationStartup 类的完整实现。实现自定义启动逻辑的一个原因是在上下文创建期间实时观察步骤。这样,如果需要,我们可以立即采取措施,例如在 Bean 创建耗时较长时发送消息,而不是等待整个上下文创建完成。

8. 结语

在本文中,我们研究了 Spring 如何通过 ApplicationStartup 接口追踪其应用上下文的创建。

具体来说,我们介绍了 Spring 提供的三种实现

  • DefaultApplicationStartup
  • BufferingApplicationStartup
  • FlightRecorderApplicationStartup

在所有情况下,我们学习了如何访问记录的步骤以及如何向时间线添加更多步骤。最后,我们快速了解了如何插入 ApplicationStartup 接口的自定义实现。

像往常一样,本文的代码可在 GitHub 上获取

本文最初发表于 Baeldung