SpringSPI 踩坑记录
问题表现:SpringSPI 的 @Configuration 类中某一种特殊场景下出现了循环依赖,但 Spring 并没有正确处理循环依赖(尝试解决或者抛出异常阻止程序启动),而是忽略掉有循环依赖的Bean的注入,并正常启动程序,导致问题隐蔽不容易被发现。
复习一下循环依赖
循环依赖简而言之就是:
@Componet
public class BeanA {
@Resource
private BeanB beanB;
}
@Componet
public class BeanB {
@Resource
private BeanA beanA;
}
除了上面这种显式的循环依赖,还有一种循环依赖是隐式的:
@Componet
public class BeanA {
@Resource
private BeanB beanB;
}
@Componet
public class BeanB {
@Resource
private ApplicationContext context;
@PostConstruct
public void init(){
// 从 context 中 get Bean 的行为会触发 Bean 的实例化,也会带来隐式的循环依赖问题
List<BeanA> beanAList = context.getBeansOfType(BeanA.class);
}
}
通常情况下,Spring 框架能比较负责地处理好循环依赖的问题,我们熟悉的方案就是用三级缓存Bean来处理,或者在无法处理循环依赖时解析并抛出异常,阻止程序启动,所以我们可以认为在Spring的默认行为中,所有的依赖都应该被正确注入到正确位置,否则程序不会启动。
问题情景复刻
有一个使用 Spring SPI 的 starter 模块:bad-starter,以及我们自己的 SpringBoot 程序模块 my-app。my-app 会引用 bad-starter 这个包,作为开发者,我们其实很少关心 starter 包内部的具体实现。
bad-starter 中提供了一个接口 HandlerBean,当我们自己的 Bean 实现这个接口的时候,SDK 内部提供的 HandlerFactory 这个 Bean 就会自动注入我们实现的 Bean 来进行功能的扩展,这种扩展方式十分常见:
@Component
public class HandlerFactory {
@Resource
private ApplicationContext context;
private Map<String,HandlerBean> handlerMap;
@PostConstruct
public void init(){
List<HandlerBean> handlerList = context.getBeansOfType(HandlerBean.class);
handlerMap = handlerList.stream().collect(Collectors.toMap(HandlerBean::getType,handler->handler))
}
}
这个 bad-starter 的 SDK 中也提供了一些其他的 Bean,比如 BadService,以这个 Bean 为例,有的 Hendler 中也需要使用 SDK 提供的其他 Bean 来实现某些功能。我们定义了两个 Handler,分别如下:
@Component
public class HandlerA implements HandlerBean {
@Override
public String getType(){
return "A";
}
}
@Component
public class HandlerB implements HandlerBean {
@Resource
private BadService badService;
@Override
public String getType(){
return "B";
}
}
然后启动程序,没有任何报错,对 HandlerFactory 进行逻辑测试,发现:HandlerFactory 里的 HandlerB 失效了!
用一些小手段探查了一下 Spring 上下文里的 Bean 实例,发现 HandlerB 实例确实在容器里,然后日志检查了一下 Bean 的实例化顺序,发现 HandlerFactory 的 @PostConstruct 方法执行的时间点竟然在 HandlerB 的实例化之前。
探寻原因
当我们第一次发现 Bean 的注入失效的时候,快速决策出的第一个方案是要求 bad-stater 方修改了 Bean 的获取方式,以保证所有的 Bean 都能正确注入。
@Component
public class HandlerFactory {
@Resource
private ApplicationContext context;
@Resource
private List<HandlerBean> handlerList;
private Map<String,HandlerBean> handlerMap;
@PostConstruct
public void init(){
// List<HandlerBean> handlerList = context.getBeansOfType(HandlerBean.class);
handlerMap = handlerList.stream().collect(Collectors.toMap(HandlerBean::getType,handler->handler))
}
}
然后再次启动程序测试时发现,程序无法启动了,Spring 判断出现了循环依赖:BadService 循环依赖了 HandlerFactory 这个 Bean。看起来就是因为 BadService 和 HandlerFactory 循环依赖导致我们的 Bean 没有被正确注入。但原因真的是我们想象的这么简单吗?
首先,我们都知道 SpringBoot 框架中,从 context 中 get Bean 的行为会也触发 Bean 的实例化,从而带来隐式的循环依赖问题。而这个隐式的循环依赖问题 Spring 会主动解决,以保证所有的Bean都能正确注入依赖。一般情况下,我们会默认 ApplicationContext 的 getBean 构成的隐式依赖关系是可靠的,不会存在有依赖但没注入的情况。
举个例子,BeanA 和 BeanB 构成隐式的循环依赖关系:
@Service
public class BeanA {
@Resource
private ApplicationContext context;
@PostConstruct
public void init(){
BeanB bean = context.getBean(BeanB.class);
System.out.println(bean.getClass());
}
}
@Service
public class BeanB {
@Resource
private ApplicationContext context;
@PostConstruct
public void init(){
BeanA bean = context.getBean(BeanA.class);
System.out.println(bean.getClass());
}
}
// 输出:
// class priv.dawn.lab17.BeanA
// class priv.dawn.lab17.BeanB
其次,三个 Bean 的通过 @Resource 注入造成的循环依赖,Spring 不可能解决不了。
当然,最让我觉得这个 Bug 不可思议的地方在于:当时出现循环依赖问题的 BadService 是个 RPC 接口客户端,这怎么可能依赖自定义的 HandlerFactory!
当时为了快速解决问题,采用的方式是@Laze
懒加载解决了循环依赖问题。问题虽然解决了,困扰始终没有消散,这个场景太诡异了,一个隐式的循环依赖问题,一个不应该有循环依赖关系的依赖场景,以及没有注入 Bean 却能正常启动的程序。
于是经过三天的实验和复查,终于将bug复现出来。导致这个Bug的原因出现在 bad-starter 的 spi configuration 上,在 bad-stater 中,有一个在 SPI 流程中注入的 @Configuration 是这样的:
@Configuration
public class BadConfiguration {
@Resource
private HandlerFactory handlerFactory;
@Bean
public BadService badService(){
return ...
}
...
}
BadConfiguration 依赖了 HandlerFactory,HandlerFactory 隐式依赖了 HandlerB,HandlerB 依赖了 BadConfiguration 中的 BadService,从而导致了循环依赖。
这种配置如果在自己启动程序的 Bean 的扫描路径上,完全没有什么问题,你依然会发现 HandlerB 被成功注入进 HandlerFactory。但是如果这个配置是 SPI 环节注入时的配置,Spring 的行为却完全不一样,当 HandlerFactory 试图通过 context 获取所有的 HandlerBean 时,在循环依赖链中的 HandlerB 被直接忽略掉,不会注入进 HandlerFactory,也不会报错。
大家可以自己测试一下,SPI 就用 spring.factories 实现:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.bad.spring.BadConfiguration
为什么说这个问题是个隐秘的大坑
这个问题情景里面,依赖没有正确注入,程序却正常启动(甚至没有日志警告),只有当业务逻辑出现异常时才能发现问题,这显然是个隐蔽且危害不小的隐患。
排查这个 bug 也害的我掉了不少头发,最后给大家总结两点经验:
- 在提供 SPI 的 Configuration 时,有相同依赖的 Bean 才聚合在一个 Configuration 中,不同依赖链之间一定要相互隔离在不同 Configuration 类中。
- 在处理需要注入 Bean 列表的时候,强制要求使用 @Resource List 来注入,保证所有的 Bean 都能注入,或者触发循环依赖异常快速失败阻止程序启动。