功能介绍

Target:针对启动慢的 Spring 应用,找出 IOC 容器启动过程中,加载耗时较长的 Bean 对象进行治理。

实现原理

主要用到Spring本身提供的两个扩展接口:BeanPostProcessor ApplicationListener

这两个接口,在Spring框架中,还是比较常见的组件,原理和使用方式自提:

Spring中的BeanPostProcessor接口、Spring监听器用法与原理详解

Spring Bean加载耗时采集工具_ide

代码示例

@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