问题呈现
项目性能优化,需要将本地内存(JVM内存)替换为本地Redis(同一个Pod中的Container),降低JVM内存和GC的压力,同时引入了JetCache简化和统一使用(对JetCache也做了扩展,支持了本地Redis)。引入JetCache后,项目启动就会报循环依赖的错误,错误入口则是项目中使用@PostConstruct调用JetCache加载预热缓存的地方,错误堆栈如下:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'globalCacheConfig' defined in class path resource [cn/jojo/edu/jetcache/autoconfigure/JetCacheAutoConfiguration.class]: Circular depends-on relationship between 'globalCacheConfig' and 'redissonAutoInit'at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:305) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:227) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1155) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:416) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.init(LazyInitCache.java:83) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.checkInit(LazyInitCache.java:66) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.put(LazyInitCache.java:159) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.malacca.integration.cache.AbstractLocalRedisCacheService.put2Cache(AbstractLocalRedisCacheService.java:92) ~[classes/:?]at cn.jojo.edu.malacca.integration.cache.AbstractLocalRedisCacheService.forceRefreshCache(AbstractLocalRedisCacheService.java:140) ~[classes/:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.execute(PreheatInit.java:96) ~[classes/:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.lambda$init$0(PreheatInit.java:73) ~[classes/:?]at cn.jojo.infra.sdk.context.request.RequestContextHolder.setContext(RequestContextHolder.java:162) ~[microservice-sdk-1.7.29.jar:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.init(PreheatInit.java:71) ~[classes/:?]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_65]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_65]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_65]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_65]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]... 18 more
看错误日志问题是出在预热调用LazyInitCache.put方法时,该方法内部首先会获取GlobalCacheConfig对象,让其优先初始化,就是在初始化该对象时报的循环依赖错误:
存在以下几个奇怪的问题:
- 错误提示的是globalCacheConfig和redissonAutoInit产生了循环依赖,但是看GlobalCacheConfig本身并没有依赖其它复杂的对象,只是在初始化时依赖了SpringConfigProvider、JetCacheProperties和AutoConfigureBeans,globalCacheConfig和redissonAutoInit的依赖是如何产生的呢?
- 为什么在@PostConstruct中预热会报错,但在@EventListener(ApplicationReadyEvent.class)中预热却不会报错?
- 为什么配置启用了多个缓存组件才会报错,只启用一个缓存组件却不会?
- Spring通过三级缓存解决了非构造函数注入产生的循环依赖,那为什么这个循环依赖没有被解决呢?
分析过程
依赖关系分析
RedissonAutoInit对GobalCacheConfig的依赖比较好发现,从图中可以看到是父类中引用了ConfigProvider,在ConfigProvider中又引用了GlobalCacheConfig,不过从图中也无法找出GobalCacheConfig对RedissonAutoInit的依赖从何而来。
于是查看报循环依赖错误的源码:
这部分源码位于AbstractBeanFactory.doGetBean方法中,在创建Bean之前会对有设置dependsOn属性的BeanDefinition(以下简称BD)校验是否有循环依赖,但是GlobalCacheConfig类中并没有标记@DependsOn注解,还有哪里可以设置呢?不难想到Spring本身提供的一些扩展点是可以修改BD属性的,比如BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor,最终找到一个BeanFactoryPostProcessor的实现类BeanDependencyManager:
public class BeanDependencyManager implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {String[] autoInitBeanNames = beanFactory.getBeanNamesForType(AbstractCacheAutoInit.class, false, false);if (autoInitBeanNames != null) {BeanDefinition bd = beanFactory.getBeanDefinition(JetCacheAutoConfiguration.GLOBAL_CACHE_CONFIG_NAME);String[] dependsOn = bd.getDependsOn();if (dependsOn == null) {dependsOn = new String[0];}int oldLen = dependsOn.length;dependsOn = Arrays.copyOf(dependsOn, dependsOn.length + autoInitBeanNames.length);System.arraycopy(autoInitBeanNames,0, dependsOn, oldLen, autoInitBeanNames.length);bd.setDependsOn(dependsOn);}}}
在这里将获取到的AbstractCacheAutoInit的子类都设置到了GlobalCacheConfig的dependsOn属性中(题外话这里并不是所有实现了AbstractCacheAutoInit的子类都会设置进去,还要看是否满足条件,Jetache提供了JetCacheCondition,只有配置启用了的缓存组件对应的Init才会初始化并设置到dependsOn中)。
至此,循环依赖就产生了。
循环依赖错误根因分析
有了循环依赖,但为什么Spring的三级缓存没有解决该问题,以及为什么是在某些条件下才会报错呢?本节就对剩余几个问题进行分析。
上面的时序图是在启用了Caffeine和Redisson缓存且使用@PostConstruct预热缓存的前提下的启动初始化过程,首先BeanDependencyManager获取到CaffeineAutoInit和RedissonAutoInit对象并设置到GlobalCacheConfig BD的dependsOn属性中。接着在@PostConstruct标记的方法执行,调用put预热缓存,在put中会优先调用getBean初始化GlobalCacheConfig。
GlobalCacheConfig初始化时就会进入到这段代码,循环依次判断依赖的对象是否有循环依赖以及调用getBean对其进行初始化。
首先是CaffeineAutoInit,在判断无循环依赖后会注册CaffeineAutoInit -> GlobalCacheConfig的依赖关系,表示CaffeineAutoInit依赖GlobalCacheConfig对象(下文同理);注册依赖关系后调用getBean初始化CaffeineAutoInit时其实也会进入到这个方法,只不过该对象没有dependsOn,所以直接跳过了这段代码,进入到实例化的逻辑:
熟悉Spring初始化逻辑的就知道该方法中创建完Bean就会对其进行依赖注入,而CaffeineAutoInit的父类中依赖了SpringConfigProvider对象:
因此又会实例化SpringConfigProvider对象,同样的该对象中又依赖了GlobalCacheConfig,所以又再次触发GlobalCacheConfig的创建,所以会再一次判断GlobalCacheConfig depnedsOn的对象和自己是否有循环依赖。
因为CaffeineAutoInit在前面已经初始化完成了,所以这次只是简单判断一下,接着判断并初始化RedissonAutoInit,这里又会注册RedissonAutoInit -> GlobalCacheConfig的依赖关系,然后又依赖注入SpringConfigProvider,依赖注入完成后会注册SpringConfigProvider -> RedissAutoInit的依赖关系(这里就是导致报错的关键步骤)。
到这里,GlobalCacheConfig注入到SpringConfigProvider完成,进入到图中第六步操作,注册GlobalCacheConfig -> SpringConfigProvider依赖关系,紧接着SpringConfigProvider注入到CaffeineAutoInit完成,注册SpringConfigProvider -> CaffeineAutoInit依赖关系。然后又一次判断并初始化RedissonAutoInit:
private boolean isDependent(String beanName, String dependentBeanName, @Nullable Set<String> alreadySeen) {if (alreadySeen != null && alreadySeen.contains(beanName)) {return false;}String canonicalName = canonicalName(beanName);Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);if (dependentBeans == null) {return false;}if (dependentBeans.contains(dependentBeanName)) {return true;}for (String transitiveDependency : dependentBeans) {if (alreadySeen == null) {alreadySeen = new HashSet<>();}alreadySeen.add(beanName);if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) {return true;}}return false;}
dependentBeanMap就是存储依赖关系的容器,canonicalName则是globalCacheConfig,dependentBeanName是redissonAutoInit,所以dependentBeans就是springConfigProvider,递归再次判断就有了R -> S -> G - R的循环依赖。
看到这,对于剩余几个问题就很容易理解了,下面总结概括下:
当在@PostConstruct中调用put方法保存缓存时,会优先创建初始化GlobalCacheConfig对象,该对象存在dependsOn依赖的对象,Spring会对有dependsOn的BD在实例化之前判断是否有循环依赖,当dependsOn只有一个时,三级缓存帮我们解决了循环依赖的问题,不会报错;而当有多个时,实例化第一个依赖的对象CaffeineAutoInit时,会导致GlobalCacheConfig的嵌套创建,在第二次创建时,第二个依赖对象RedissonAutoInit会注册依赖关系,返回到首次创建的地方再次判断RedissonAutoInit的依赖关系,就得到循环依赖的结果。而当GlobalCacheConfig延迟主动实例化时(只要在它之前随便先创建一个Init对象),直接就从单例缓存中就获取到了对象(依赖注入会创建该对象并放入缓存),进入不到这个判断,所以就不会报错(依赖注入触发的GlobalCacheConfig实例化虽然会进入这个判断,但不会导致嵌套创建),感兴趣的可以自行画一下创建过程。
最后还有个问题,为什么Spring要在createBean之前对有dependsOn属性的BD判断是否有循环依赖,是为了应对什么场景?没有这个判断,针对这个场景三级缓存是否也能解决循环依赖的问题呢?