Spring Bean加载耗时采集工具
功能介绍
Target:针对启动慢的 Spring 应用,找出 IOC 容器启动过程中,加载耗时较长的 Bean 对象进行治理。
实现原理
主要用到Spring本身提供的两个扩展接口:BeanPostProcessor ApplicationListener
这两个接口,在Spring框架中,还是比较常见的组件,原理和使用方式自提:
Spring中的BeanPostProcessor接口、Spring监听器用法与原理详解
代码示例
@Component
public class SpringBeanLoadTimeCollectorimplements BeanPostProcessor, ApplicationListener<ContextRefreshedEvent> {private final Logger logger = LoggerFactory.getLogger(this.getClass());private final ConcurrentMap<String, Instant> beanCreationStartTime = new ConcurrentHashMap<>();// Thread-safe and orderedprivate final Map<String, Long> beanLoadTime = Collections.synchronizedMap(new LinkedHashMap<>());private static final String logFileName = "springbean-loadtime.log";@Overridepublic 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 projectif (bean.getClass().getPackage().getName().startsWith("com.alibaba")) {beanCreationStartTime.putIfAbsent(beanName, Instant.now());}return bean;}@Overridepublic 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;}@Overridepublic void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {// Ensure that we are in the root contextif (contextRefreshedEvent.getApplicationContext().getParent() == null) {// Now that the context is fully loaded, print the bean load time mapwriteBeanLoadTimeLog();}}private void writeBeanLoadTimeLog() {BeanLoadTimeResult result = new BeanLoadTimeResult(beanLoadTime);// Write the results to a log filedoWriteLog(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;}@Overridepublic 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