ApplicationContext 管理 Spring 应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以有同样复杂的组件图和启动阶段。
用具体的指标来跟踪应用程序的启动步骤,可以帮助了解在启动阶段花费的时间,但它也可以作为一种方式来更好地了解整个上下文生命周期。
AbstractApplicationContext(及其子类)被一个 ApplicationStartup 工具化,它收集关于各种启动阶段的StartupStep 数据。
- 应用环境生命周期(基础包扫描、配置类管理)。
- Bean 生命周期(实例化,智能初始化,后置处理)
- 应用程序事件处理
下面是一个在 AnnotationConfigApplicationContext 中使用这个跟踪功能的例子(AnnotationConfigApplicationContext 中的部分源码):
// 创建一个启动步骤并开始记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 给当前步骤添加标签信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们正在检测的实际阶段
this.scanner.scan(basePackages);
// 结束当前步骤
scanPackages.end();
从代码语义来看,就是有将扫描这一步骤进行耗时检测之类的。
应用程序的上下文已经有了多个步骤的工具。一旦被记录下来,这些启动步骤就可以用特定的工具进行收集、显示和分析。关于现有启动步骤的完整列表,你可以查看 专门的附录部分。
默认的 ApplicationStartup 实现是一个无操作的变体,以最小的开销。这意味着在应用启动过程中,默认不会收集任何指标。Spring Framework 提供了一个 FlightRecorderApplicationStartup 实现,用于跟踪 Java Flight Recorder 的启动步骤。。要使用这个变体,你必须在 ApplicationContext 创建后立即将它的一个实例配置到 ApplicationContext 中。
如果开发者提供自己的 AbstractApplicationContext 子类,或者希望收集更精确的数据,他们也可以使用 ApplicationStartup 基础设施。
:::tips ApplicationStartup 仅用于应用程序的启动和核心容器;这绝不是 Java 剖析器或 Micrometer 等指标库的替代。 :::
为了开始收集自定义的 StartupStep,组件可以直接从应用上下文中获取 ApplicationStartup 实例,使其组件实现 ApplicationStartupAware,或者在任何注入点上请求 ApplicationStartup 类型。
:::info 开发人员在创建自定义启动步骤时不应使用 「spring.*」命名空间。这个命名空间是为 Spring 内部使用而保留的,并且会有变化。 :::
ApplicationStartup 目前有 3 个实现类:
- DefaultApplicationStartup,它不进行跟踪,这个是默认配置的实现
- FlightRecorderApplicationStartup 不太明白这个记录到哪里了,他的源码上有说,需要在 jvm 参数上传递配置
- BufferingApplicationStartup:这个将跟踪的数据记录到了内存中,下面我们以这个作为例子看看如何使用
配置 BufferingApplicationStartup 作为跟踪器
package cn.mrcode.study.springdocsread;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.metrics.StartupStep;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import cn.mrcode.study.springdocsread.web.AppConfig;
/**
* @author zhuqiang
* @date 2022/2/10 11:29
*/
public class TestDemo {
public static void main(String[] args) {
// new AnnotationConfigApplicationContext(AppConfig.class)
// 不能将配置类直接传递给构造函数;因为默认会直接注册和调用 refresh() 方法,我们就来不及配置我们自己的启动器了
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
// 配置启动器,并设置保留 1000 个事件
ctx.setApplicationStartup(new BufferingApplicationStartup(1000));
// 设置配置类,这个配置类,前面讲过就是你自己的配置类,想要扫描哪些东西之类的
ctx.register(AppConfig.class);
// 容器开始启动
ctx.refresh();
// 启动完成后,下面获取这个跟踪器,从里面拿到 容器启动过程中收集到的信息
final BufferingApplicationStartup bean = ctx.getBean(BufferingApplicationStartup.class);
final List<StartupTimeline.TimelineEvent> events = bean.getBufferedTimeline().getEvents();
for (StartupTimeline.TimelineEvent event : events) {
// 里面定义的是这个步骤的名称和标签等信息
final StartupStep startupStep = event.getStartupStep();
final String name = startupStep.getName();
final StartupStep.Tags tags = startupStep.getTags();
String tagStr = "";
for (StartupStep.Tag tag : tags) {
tagStr += ("key=" + tag.getKey() + "; value=" + tag.getValue());
}
System.out.println("name=" + name + " ;tagStr=" + tagStr);
// 下面输出这个步骤的开始和结束时间,还有耗时信息
final Instant startTime = event.getStartTime();
final Instant endTime = event.getEndTime();
final Duration duration = event.getDuration();
System.out.println("耗时:开始时间=" + startTime + "; 结束时间=" + endTime + "; 耗时=" + duration);
}
}
}
配置类信息:我这里就使用了一个 @ComponentScan
扫描自己的其他业务类
package cn.mrcode.study.springdocsread.web;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author mrcode
*/
@Configuration
@ComponentScan("cn.mrcode.study.springdocsread.event")
public class AppConfig{
}
执行之后输出的信息如下
name=spring.context.component-classes.register ;tagStr=key=classes; value=[class cn.mrcode.study.springdocsread.web.AppConfig]
耗时:开始时间=2022-02-25T10:09:19.360Z; 结束时间=2022-02-25T10:09:19.412Z; 耗时=PT0.052S
name=spring.beans.instantiate ;tagStr=key=beanName; value=org.springframework.context.annotation.internalConfigurationAnnotationProcessorkey=beanType; value=interface org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
耗时:开始时间=2022-02-25T10:09:19.430Z; 结束时间=2022-02-25T10:09:19.448Z; 耗时=PT0.018S
name=spring.context.config-classes.parse ;tagStr=key=classCount; value=2
耗时:开始时间=2022-02-25T10:09:19.458Z; 结束时间=2022-02-25T10:09:19.505Z; 耗时=PT0.047S
name=spring.context.beandef-registry.post-process ;tagStr=key=postProcessor; value=org.springframework.context.annotation.ConfigurationClassPostProcessor@1bb5a082
耗时:开始时间=2022-02-25T10:09:19.448Z; 结束时间=2022-02-25T10:09:19.505Z; 耗时=PT0.057S
name=spring.context.config-classes.enhance ;tagStr=key=classCount; value=1
耗时:开始时间=2022-02-25T10:09:19.515Z; 结束时间=2022-02-25T10:09:19.582Z; 耗时=PT0.067S
name=spring.context.bean-factory.post-process ;tagStr=key=postProcessor; value=org.springframework.context.annotation.ConfigurationClassPostProcessor@1bb5a082
耗时:开始时间=2022-02-25T10:09:19.515Z; 结束时间=2022-02-25T10:09:19.582Z; 耗时=PT0.067S
name=spring.beans.instantiate ;tagStr=key=beanName; value=org.springframework.context.event.internalEventListenerProcessorkey=beanType; value=interface org.springframework.beans.factory.config.BeanFactoryPostProcessor
耗时:开始时间=2022-02-25T10:09:19.582Z; 结束时间=2022-02-25T10:09:19.585Z; 耗时=PT0.003S
name=spring.beans.instantiate ;tagStr=key=beanName; value=org.springframework.context.event.internalEventListenerFactory
耗时:开始时间=2022-02-25T10:09:19.585Z; 结束时间=2022-02-25T10:09:19.585Z; 耗时=PT0S
name=spring.context.bean-factory.post-process ;tagStr=key=postProcessor; value=org.springframework.context.event.EventListenerMethodProcessor@53045c6c
耗时:开始时间=2022-02-25T10:09:19.585Z; 结束时间=2022-02-25T10:09:19.585Z; 耗时=PT0S
name=spring.beans.instantiate ;tagStr=key=beanName; value=org.springframework.context.annotation.internalAutowiredAnnotationProcessorkey=beanType; value=interface org.springframework.beans.factory.config.BeanPostProcessor
耗时:开始时间=2022-02-25T10:09:19.587Z; 结束时间=2022-02-25T10:09:19.590Z; 耗时=PT0.003S
name=spring.beans.instantiate ;tagStr=key=beanName; value=org.springframework.context.annotation.internalCommonAnnotationProcessorkey=beanType; value=interface org.springframework.beans.factory.config.BeanPostProcessor
耗时:开始时间=2022-02-25T10:09:19.590Z; 结束时间=2022-02-25T10:09:19.593Z; 耗时=PT0.003S
name=spring.context.beans.post-process ;tagStr=
耗时:开始时间=2022-02-25T10:09:19.425Z; 结束时间=2022-02-25T10:09:19.594Z; 耗时=PT0.169S
name=spring.beans.instantiate ;tagStr=key=beanName; value=appConfig
耗时:开始时间=2022-02-25T10:09:19.597Z; 结束时间=2022-02-25T10:09:19.604Z; 耗时=PT0.007S
name=spring.beans.instantiate ;tagStr=key=beanName; value=eventPub
耗时:开始时间=2022-02-25T10:09:19.604Z; 结束时间=2022-02-25T10:09:19.606Z; 耗时=PT0.002S
name=spring.beans.smart-initialize ;tagStr=key=beanName; value=org.springframework.context.event.internalEventListenerProcessor
耗时:开始时间=2022-02-25T10:09:19.606Z; 结束时间=2022-02-25T10:09:19.622Z; 耗时=PT0.016S
name=spring.context.refresh ;tagStr=
耗时:开始时间=2022-02-25T10:09:19.412Z; 结束时间=2022-02-25T10:09:19.636Z; 耗时=PT0.224S
这些步骤全是 spring 的核心启动是做的一些操作,每一个步骤的含义可以看 专门的附录部分 中去找对应这个步骤的的功能是什么,这样就可以了解整个容器启动的大概过程和耗时。
我们自己也可以使用这个跟踪器。下面是一个例子
单独使用跟踪器
package cn.mrcode.study.springdocsread;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.core.metrics.StartupStep;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author zhuqiang
* @date 2022/2/10 11:29
*/
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
final BufferingApplicationStartup bean = new BufferingApplicationStartup(1000);
// 开始一个步骤
final StartupStep start = bean.start("测试步骤");
start.tag("标签1","标签值");
TimeUnit.SECONDS.sleep(10); // 模拟耗时 10 秒
start.end(); // 结束这个业务
// 启动完成后,下面获取这个跟踪器,从里面拿到 容器启动过程中收集到的信息
final List<StartupTimeline.TimelineEvent> events = bean.getBufferedTimeline().getEvents();
for (StartupTimeline.TimelineEvent event : events) {
// 里面定义的是这个步骤的名称和标签等信息
final StartupStep startupStep = event.getStartupStep();
final String name = startupStep.getName();
final StartupStep.Tags tags = startupStep.getTags();
String tagStr = "";
for (StartupStep.Tag tag : tags) {
tagStr += ("key=" + tag.getKey() + "; value=" + tag.getValue());
}
System.out.println("name=" + name + " ;tagStr=" + tagStr);
// 下面输出这个步骤的开始和结束时间,还有耗时信息
final Instant startTime = event.getStartTime();
final Instant endTime = event.getEndTime();
final Duration duration = event.getDuration();
System.out.println("耗时:开始时间=" + startTime + "; 结束时间=" + endTime + "; 耗时=" + duration);
}
}
}
输出信息如下
name=测试步骤 ;tagStr=key=标签1; value=标签值
耗时:开始时间=2022-02-25T10:08:37.710Z; 结束时间=2022-02-25T10:08:47.728Z; 耗时=PT10.018S