功能介绍
Target:针对启动慢的 Spring 应用,找出 IOC 容器启动过程中,加载耗时较长的 Bean 对象进行治理。
实现原理
主要用到Spring本身提供的两个扩展接口:BeanPostProcessor ApplicationListener
这两个接口,在Spring框架中,还是比较常见的组件,原理和使用方式自提:
Spring中的BeanPostProcessor接口、Spring监听器用法与原理详解
代码示例
@Component
public class SpringBeanLoadTimeCollector
implements BeanPostProcessor, ApplicationListener<ContextRefreshedEvent> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final ConcurrentMap<String, Instant> beanCreationStartTime = new ConcurrentHashMap<>();
// Thread-safe and ordered
private final Map<String, Long> beanLoadTime = Collections.synchronizedMap(new LinkedHashMap<>());
private static final String logFileName = "springbean-loadtime.log";
@Override
public Object postProcessBeforeInitialization(@NotNull Object bean, @NotNull String beanName) {
// 环境变量方式:
// java -Dproject.package.prefix=项目下的包路径前缀 -jar your-app.jar
// System.getProperty("project.package.prefix", "项目下的包路径前缀")
// The prefix of the package path under the project
if (bean.getClass().getPackage().getName().startsWith("com.alibaba")) {
beanCreationStartTime.putIfAbsent(beanName, Instant.now());
}
return bean;
}
@Override
public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) {
Instant startTime = beanCreationStartTime.get(beanName);
if (startTime != null) {
Instant endTime = Instant.now();
Duration duration = Duration.between(startTime, endTime);
beanLoadTime.put(beanName, duration.toMillis());
}
return bean;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// Ensure that we are in the root context
if (contextRefreshedEvent.getApplicationContext().getParent() == null) {
// Now that the context is fully loaded, print the bean load time map
writeBeanLoadTimeLog();
}
}
private void writeBeanLoadTimeLog() {
BeanLoadTimeResult result = new BeanLoadTimeResult(beanLoadTime);
// Write the results to a log file
doWriteLog(result.toString());
}
private void doWriteLog(String log) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFileName))) {
writer.write(log);
} catch (IOException e) {
logger.error("Error writing bean load times to file error", e);
}
}
public static class BeanLoadTimeResult {
public final int count;
public final String unit;
public final Map<String, Long> beanLoadTimeMap;
BeanLoadTimeResult(Map<String, Long> beanLoadTimeMap) {
this.unit = "ms";
this.count = beanLoadTimeMap.size();
this.beanLoadTimeMap = beanLoadTimeMap;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
}
采集结果
{
"beanLoadTime": {
"beanName_1": 10, // bean名称和加载耗时时间(单位毫秒)
"beanName_2": 200,
"beanName_3": 102,
"beanName_4": 59,
......
},
"total": 267, // 工程中被Spring托管的总Bean数量
"unit": "ms"
}
补充知识
-
Bean 异步加载
Spring官方不推荐,核心应用不建议使用(已踩坑)。但是从使用效果看,这个可以明显提升启动速度!
-
Bean 并行加载
看过几篇相关帖子,方案是可行的,不过目前还没有落地实现。
Bean并行加载的难点:
Bean之间的循环依赖关系如何在并行加载的同时保证初始化、实例化准确无误?
对于多个Bean的复杂依赖关系(例如树状结构),在并行加载时,要考虑到多个线程之间共享一份树的遍历索引,避免重复加载。
-
Bean 懒加载
Spring Framework 5.2 和 Spring Boot 2.2 引入了 improve 全局懒初始化,可以减少启动时的 CPU 和内存开销,进而提升启动速度。
可以通过下面的配置启用:
spring.main.lazy-initialization=true