公司入职一个阿里大佬,把 Spring Boot 系统启动时间从 7 分钟降到了 40 秒!(2)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 公司入职一个阿里大佬,把 Spring Boot 系统启动时间从 7 分钟降到了 40 秒!

1.2 监控 Bean 注入耗时

第二个优化的思路是监控所有 Bean 对象初始化的耗时,即每个 Bean 对象实例化、初始化、注册所花费的时间,有没有特别耗时 Bean 对象?


同样的,我们可以利用 SpringBoot 提供了 BeanPostProcessor 接口来监控 Bean 的注入耗时,BeanPostProcessor 是 Spring 提供的 Bean 初始化前后的 IOC 钩子,用于在 Bean 初始化的前后执行一些自定义的逻辑:



public interface BeanPostProcessor {
    // 初始化前
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    // 初始化后
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

对于 BeanPostProcessor 接口的实现类,其前后置处理过程体现在 AbstractAutowireCapableBeanFactory#doCreateBean,这也是 Spring 中非常重要的一个方法,用于真正实例化 Bean 对象,通过 BeanFactory#getBean 方法一路 Debug 就能找到。在该方法中调用了 initializeBean 方法:


protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
    ...
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 应用所有 BeanPostProcessor 的前置方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    try {
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
    if (mbd == null || !mbd.isSynthetic()) {
        // 应用所有 BeanPostProcessor 的后置方法
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}



通过 BeanPostProcessor 原理,在前置处理时记录下当前时间,在后置处理时,用当前时间减去前置处理时间,就能知道每个 Bean 的初始化耗时,下面是我的实现:


@Component
public class TimeCostBeanPostProcessor implements BeanPostProcessor {
    private Map<String, Long> costMap = Maps.newConcurrentMap();
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        costMap.put(beanName, System.currentTimeMillis());
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (costMap.containsKey(beanName)) {
            Long start = costMap.get(beanName);
            long cost  = System.currentTimeMillis() - start;
            if (cost > 0) {
                costMap.put(beanName, cost);
                System.out.println("bean: " + beanName + "\ttime: " + cost);
            }
        }
        return bean;
    }
}




BeanPostProcessor 的逻辑是在 Beanfactory 准备好后处理的,就不需要通过 SpringFactoriesLoader 加载了,直接 @Component 注入即可。


重启服务,通过以上方法排查 Bean 初始化过程,还真的有所发现:


image.png


这个 Bean 初始化耗时43s,具体看下这个 Bean 的初始化方法,发现会从数据库查询大量配置元数据,并更新到 Redis 缓存中,所以初始化非常慢:




image.png

另外,还发现了一些非项目自身服务的service、controller对象,这些 Bean 来自于第三方依赖:UPM服务,项目中并不需要:



image.png


其实,原因上文已经提到:我只接入一个功能,但我注入了该服务路径下所有的 Bean,也就是说,服务里注入其他服务的、对自身无用的 Bean。


Spring Boot 基础就不介绍了,推荐看这个免费教程:


https://github.com/javastacks/spring-boot-best-practice


2 优化方案

2.1 如何解决扫描路径过多?

想到的解决方案比较简单粗暴:


梳理要引入的 Bean,删掉主配置类上扫描路径,使用 JavaConfig 的方式显式手动注入。


以 UPM 的依赖为例,之前的注入方式 是,项目依赖其 UpmResourceClient 对象,Pom 已经引用了其 Maven 坐标,并在主配置类上的 scanBasePackages 中添加了其服务路径:"com.xxx.ad.upm",通过扫描整个服务路径下的 class,找到 UpmResourceClient 并注入,因为该类注解了 @Service,因此会注入到服务的 Spring 上下文中,UpmResourceClient 源码片段及主配置类如下:  


image.png

image.png


使用 JavaConfig 的改造方式是:不再扫描 UPM 的服务路径,而是主动注入。删除"com.xxx.ad.upm",并在服务路径下添加以下配置类:


@Configuration
public class ThirdPartyBeanConfig {
    @Bean
    public UpmResourceClient upmResourceClient() {
        return new UpmResourceClient();
    }
}



Tips:如果该 Bean 还依赖其他 Bean,则需要把所依赖的 Bean 都注入; 针对 Bean 依赖情况复杂的场景梳理起来就比较麻烦了,所幸项目用到的服务 Bean 依赖关系都比较简单,一些依赖关系复杂的服务,观察到其路径扫描耗时也不是很高,就不处理了。


同时,通过 JavaConfig 按需注入的方式,就不存在冗余 Bean 的情况了,也有利于降低服务的内存消耗;解决了上面的引入无关的 upmService、upmController 的问题。


2.2 如何解决 Bean 初始化高耗时?

Bean 初始化耗时高,就需要 case by case 地处理了,比如项目中遇到的初始化配置元数据的问题,可以考虑通过将该任务提交到线程池的方式异步处理或者懒加载的方式来解决。


3 新的问题

完成以上优化后,本地启动时间从之前的 7min 左右降低至 40s,效果还是非常显著的。本地自测通过后,便发布到预发进行验证,验证过程中,有同学发现项目接入的 Redis 缓存组件失效了。


该组件接入方式与上文描述的接入方式类似,通过添加扫描服务的根路径"com.xxx.ad.rediscache",注入对应的 Bean 对象;查看该缓存组件项目的源码,发现该路径下有一个 config 类注入了一个缓存管理对象 CacheManager,其实现类是 RedisCacheManager:



image.png


缓存组件代码片段:




image.png

本次优化中,我是通过 每次删除一条扫描路径,启动服务后根据启动日志中 Bean 缺失错误的信息,来逐个梳理、添加依赖的 Bean,保证服务正常启动 的方式来改造的,而删除"com.xxx.ad.rediscache"后启动服务并无异常,因此就没有进一步的操作,直接上预发验证了。这就奇怪了,既然不扫描该组件的业务代码根路径,也就没有执行注入该组件中定义的 CacheManager 对象,为啥用到缓存的地方没有报错呢?


尝试在未添加扫描路径的情况下,从 ApplicationContext 中获取 CacheManager 类型的对象看下是否存在?结果发现确实存在 RedisCacheManager 对象:



image.png


其实,前面的分析并没有错,删除扫描路径后生成的 RedisCacheManager 并不是缓存组件代码中配置的,而是 SpringBoot 的自动化配置生成的,也就是说该对象并不是我们想要的对象,是不符合预期的,下文介绍其原因。


3.1 SpringBoot 自动化装配,让人防不胜防

查阅 SpringBoot Cache 相关资料,发现 SpringBoot Cache 做了一些自动推断和注入的工作,原来是 SpringBoot 自动化装配的锅呀,接下来就分析下 SpringBoot Cache 原理,明确出现以上问题的原因。


SpringBoot 自动化配置,体现在主配置类上复合注解 @SpringBootApplication 中的@EnableAutoConfiguration 上,该注解开启了 SpringBoot 的自动配置功能。该注解中的@Import(AutoConfigurationImportSelector.class) 通过加载 META-INF/spring.factotries 下配置一系列 *AutoConfiguration 配置类,根据现有条件推断,尽可能地为我们配置需要的 Bean。这些配置类负责各个功能的自动化配置,其中用于 SpringBoot Cache 的自动配置类是 CacheAutoConfiguration,接下来重点分析这个配置类就行了。


image.png


@SpringBootApplication 复合注解中集成了三个非常重要的注解:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,其中 @EnableAutoConfiguration 就是负责开启自动化配置功能;


SpringBoot 中有多 @EnableXXX 的注解,都是用来开启某一方面的功能,其实现原理也是类似的:通过 @Import 筛选、导入满足条件的自动化配置类。


可以看到 CacheAutoConfiguration 上有许多注解,重点关注下@Import({CacheConfigurationImportSelector.class}),CacheConfigurationImportSelector 实现了 ImportSelector 接口,该接口用于动态选择想导入的配置类,这个 CacheConfigurationImportSelector 用来导入不同类型的 Cache 的自动配置类:



image.png


通过调试 CacheConfigurationImportSelector 发现,根据 SpringBoot 支持的缓存类型(CacheType),提供了10种 cache 的自动配置类,按优先级排序,最终只有一个生效,而本项目中恰恰就是 RedisCacheConfiguration,其内部提供的是 RedisCacheManager,和引入第三方缓存组件一样,所以造成了困惑:


image.png


看下 RedisCacheConfiguration 的实现:


image.png


这个配置类上有很多条件注解,当这些条件都满足的话,这个自动配置类就会生效,而本项目恰恰都满足,同时项目主配置类上还加上了 @EnableCaching,开启了缓存功能,即使缓存组件没生效,SpringBoot 也会自动生成一个缓存管理对象;


即:缓存组件服务扫描路径存在的话,缓存组件中的代码生成缓存管理对象,@ConditionalOnMissingBean(CacheManager.class) 失效;扫描路径不存在的话,SpringBoot 通过推断,自动生成一个缓存管理对象。


这个也很好验证,在 RedisCacheConfiguration 中打断点,不删除扫描路径是走不到这边的SpringBoot 自动装配过程的(缓存组件显式生成过了),删除了扫描路径是能走到的(SpringBoot 自动生成)。


上文多次提到@Import,这是 SpringBoot 中重要注解,主要有以下作用: 1、导入 @Configuration 注解的类; 2、导入实现了 ImportSelector 或 ImportBeanDefinitionRegistrar 的类; 3、导入普通的 POJO。


3.2 使用 starter 机制,开箱即用

了解缓存失效的原因后,就有解决的办法了,因为是自己团队的组件,就没必要通过 JavaConfig 显式手动导入的方式改造,而是通过 SpringBoot 的 starter 机制,优化下缓存组件的实现,可以做到自动注入、开箱即用。


只要改造下缓存组件的代码,在 resources 文件中添加一个 META-INF/spring.factotries 文件,在下面配置一个 EnableAutoConfiguration 即可,这样项目在启动时也会扫描到这个 jar 中的 spring.factotries 文件,将 XxxAdCacheConfiguration 配置类自动引入,而不需要扫描"com.xxx.ad.rediscache"整个路径了:


# EnableAutoConfigurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.ad.rediscache.XxxAdCacheConfiguration



SpringBoot 的 EnableAutoConfiguration 自动配置原理还是比较复杂的,在加载自动配置类前还要先加载自动配置的元数据,对所有自动配置类做有效性筛选,具体可查阅 EnableAutoConfiguration 相关代码;



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
182 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
14天前
|
Java 数据库 数据安全/隐私保护
轻松掌握Spring依赖注入:打造你的登录验证系统
本文以轻松活泼的风格,带领读者走进Spring框架中的依赖注入和登录验证的世界。通过详细的步骤和代码示例,我们从DAO层的创建到Service层的实现,再到Spring配置文件的编写,最后通过测试类验证功能,一步步构建了一个简单的登录验证系统。文章不仅提供了实用的技术指导,还以口语化和生动的语言,让学习变得不再枯燥。
30 2
|
1月前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
191 1
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
2月前
|
存储 安全 Java
打造智能合同管理系统:SpringBoot与电子签章的完美融合
【10月更文挑战第7天】 在数字化转型的浪潮中,电子合同管理系统因其高效、环保和安全的特点,正逐渐成为企业合同管理的新宠。本文将分享如何利用SpringBoot框架实现一个集电子文件签字与合同管理于一体的智能系统,探索技术如何助力合同管理的现代化。
113 4
|
2月前
|
前端开发 Java Apache
SpringBoot实现电子文件签字+合同系统!
【10月更文挑战第15天】 在现代企业运营中,合同管理和电子文件签字成为了日常活动中不可或缺的一部分。随着技术的发展,电子合同系统因其高效性、安全性和环保性,逐渐取代了传统的纸质合同。本文将详细介绍如何使用SpringBoot框架实现一个电子文件签字和合同管理系统。
104 1
|
2月前
|
文字识别 安全 Java
SpringBoot3.x和OCR构建车牌识别系统
本文介绍了一个基于Java SpringBoot3.x框架的车牌识别系统,详细阐述了系统的设计目标、需求分析及其实现过程。利用Tesseract OCR库和OpenCV库,实现了车牌图片的识别与处理,确保系统的高准确性和稳定性。文中还提供了具体的代码示例,展示了如何构建和优化车牌识别服务,以及如何处理特殊和异常车牌。通过实际应用案例,帮助读者理解和应用这一解决方案。
|
3月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
237 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
3月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
185 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
3月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
61 3
基于Java+Springboot+Vue开发的美容预约管理系统
下一篇
DataWorks