优化方案
如何解决扫描路径过多?
想到的解决方案比较简单粗暴: 梳理要引入的 Bean,删掉主配置类上扫描路径,使用 JavaConfig 的方式显式手动注入。 以 UPM 的依赖为例,「之前的注入方式」 是,项目依赖其 UpmResourceClient 对象,Pom 已经引用了其 Maven 坐标,并在主配置类上的 scanBasePackages
中添加了其服务路径:"com.xxx.ad.upm",通过扫描整个服务路径下的 class,找到 UpmResourceClient 并注入,因为该类注解了 @Service
,因此会注入到服务的 Spring 上下文中,UpmResourceClient 源码片段及主配置类如下:
UpmResourceClientUpmResourceClient
使用 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 的问题。
如何解决 Bean 初始化高耗时?
Bean 初始化耗时高,就需要 case by case 地处理了,比如项目中遇到的初始化配置元数据的问题,可以考虑通过将该任务提交到线程池的方式异步处理或者懒加载的方式来解决。
新的问题
完成以上优化后,本地启动时间从之前的 7min 左右降低至 40s,效果还是非常显著的。本地自测通过后,便发布到预发进行验证,验证过程中,有同学发现项目接入的 Redis 缓存组件失效了。 该组件接入方式与上文描述的接入方式类似,通过添加扫描服务的根路径"com.xxx.ad.rediscache",注入对应的 Bean 对象;查看该缓存组件项目的源码,发现该路径下有一个 config 类注入了一个缓存管理对象 CacheManager
,其实现类是 RedisCacheManager
:
CacheManager
缓存组件代码片段:
RedisCacheManager
本次优化中,我是通过 「每次删除一条扫描路径,启动服务后根据启动日志中 Bean 缺失错误的信息,来逐个梳理、添加依赖的 Bean,保证服务正常启动」 的方式来改造的,而删除"com.xxx.ad.rediscache"后启动服务并无异常,因此就没有进一步的操作,直接上预发验证了。这就奇怪了,既然不扫描该组件的业务代码根路径,也就没有执行注入该组件中定义的 CacheManager
对象,为啥用到缓存的地方没有报错呢?
尝试在未添加扫描路径的情况下,从 ApplicationContext
中获取 CacheManager
类型的对象看下是否存在?结果发现确实存在 RedisCacheManager
对象:
RedisCacheManager
其实,前面的分析并没有错,删除扫描路径后生成的 RedisCacheManager
并不是缓存组件代码中配置的,而是 SpringBoot 的自动化配置生成的,也就是说该对象并不是我们想要的对象,是不符合预期的,下文介绍其原因。
SpringBoot 自动化装配,让人防不胜防
查阅 SpringBoot Cache 相关资料,发现 SpringBoot Cache 做了一些自动推断和注入的工作,原来是 SpringBoot 自动化装配的锅呀,接下来就分析下 SpringBoot Cache 原理,明确出现以上问题的原因。
SpringBoot 自动化配置,体现在主配置类上复合注解 @SpringBootApplication
中的@EnableAutoConfiguration
上,该注解开启了 SpringBoot 的自动配置功能。该注解中的@Import(AutoConfigurationImportSelector.class)
通过加载 META-INF/spring.factotries
下配置一系列 *AutoConfiguration 配置类,根据现有条件推断,尽可能地为我们配置需要的 Bean。这些配置类负责各个功能的自动化配置,其中用于 SpringBoot Cache 的自动配置类是 CacheAutoConfiguration
,接下来重点分析这个配置类就行了。
CacheAutoConfiguration
❝
@SpringBootApplication
复合注解中集成了三个非常重要的注解:@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
,其中@EnableAutoConfiguration
就是负责开启自动化配置功能; SpringBoot 中有多@EnableXXX
的注解,都是用来开启某一方面的功能,其实现原理也是类似的:通过@Import
筛选、导入满足条件的自动化配置类。❞
可以看到 CacheAutoConfiguration
上有许多注解,重点关注下@Import({CacheConfigurationImportSelector.class})
,CacheConfigurationImportSelector
实现了 ImportSelector
接口,该接口用于动态选择想导入的配置类,这个 CacheConfigurationImportSelector
用来导入不同类型的 Cache 的自动配置类:
CacheConfigurationImportSelector
通过调试 CacheConfigurationImportSelector
发现,根据 SpringBoot 支持的缓存类型(CacheType),提供了10种 cache 的自动配置类,按优先级排序,最终只有一个生效,而本项目中恰恰就是 RedisCacheConfiguration
,其内部提供的是 RedisCacheManager
,和引入第三方缓存组件一样,所以造成了困惑:
RedisCacheManager
看下 RedisCacheConfiguration
的实现:
RedisCacheConfiguration
这个配置类上有很多条件注解,当这些条件都满足的话,这个自动配置类就会生效,而本项目恰恰都满足,同时项目主配置类上还加上了 @EnableCaching
,开启了缓存功能,即使缓存组件没生效,SpringBoot 也会自动生成一个缓存管理对象;
即:缓存组件服务扫描路径存在的话,缓存组件中的代码生成缓存管理对象,@ConditionalOnMissingBean(CacheManager.class)
失效;扫描路径不存在的话,SpringBoot 通过推断,自动生成一个缓存管理对象。
这个也很好验证,在 RedisCacheConfiguration
中打断点,不删除扫描路径是走不到这边的SpringBoot 自动装配过程的(缓存组件显式生成过了),删除了扫描路径是能走到的(SpringBoot 自动生成)。
❝
上文多次提到@Import,这是 SpringBoot 中重要注解,主要有以下作用: 1、导入
@Configuration
注解的类; 2、导入实现了ImportSelector
或ImportBeanDefinitionRegistrar
的类; 3、导入普通的 POJO。❞
使用 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 相关代码;