在当下 springBoot 大环境下,我们更倾向于使用 java config 来配置和托管spring bean,而不是使用繁杂的xml,本人在使用 @Bean 去托管一个容器类 bean时,引发了一个循环依赖异常,特此记录一下(与 springBoot 版本相关)。
问题代码如下:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean
public Stu stu() {
System.out.println("put bean ...");
return new Stu();
}
@PostConstruct
private void init(){
Stu stu = stu(); // 此处直接调用 stu() 方法可能会引发循环依赖异常,这与你 springBoot 版本环境相关
System.out.println(stu);
}
}
class Stu {
}
如上代码,@PostConstruct 标记的初始化相关代码会在 @Bean 解析之前执行,在初始化代码中调用了用 @Bean 标记的方法,将会导致它提前执行,这样做可能会引发一个循环依赖异常,这与你 springBoot 的版本相关,你不能既在一个配置类中托管一个Bean,又在该配置类中使用它。
当前问题发生于 springBoot 环境 2.6.3,2.5.0 则不会发生该问题。更具体的是哪个版本改的,这里不做深究。
建议做如下修改,将 bean 的托管和 bean 的初始化相分离。
普通 Bean 的做法:
1、使用 @PostConstruct:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean
public Stu stu() {
System.out.println("put bean ...");
return new Stu();
}
}
class Stu {
@PostConstruct
private void init(){
System.out.println("stu init");
}
}
2、使用 InitializingBean 接口:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean
public Stu stu() {
System.out.println("put bean ...");
return new Stu();
}
}
class Stu implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("stu init");
}
}
3、使用 @Bean 的 initMethod:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean(initMethod = "init")
public Stu stu() {
System.out.println("put bean ...");
return new Stu();
}
}
class Stu {
public void init() {
System.out.println("stu init");
}
}
如果我们托管的是一个容器类的 bean,诸如,list,map,自身无法初始化,那么我们可以专门搞一个外部配置类来初始化它,像这样:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean(name = "list")
public List<Stu> list() {
return new CopyOnWriteArrayList<>();
}
}
@SpringBootConfiguration
@RequiredArgsConstructor
class ListInit {
private final List<Stu> list;
@PostConstruct
private void init(){
list.add(new Stu("小a"));
list.add(new Stu("小b"));
list.add(new Stu("小c"));
}
}
@AllArgsConstructor
class Stu {
private final String name;
}
总之,记住一点:不要在同一个配置类中,既配置一个 bean,又立即使用它,配置和使用它的场景应当分离。
2022年11月20日 补充:究其原因,是因为 springBoot 在更新至 2.6.0后,禁止了在注册 bean 期间发生循环引用。举个例子。
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
如上方式在 springBoot 2.6.0以前,是合理合法且自由,不受压迫的,在 2.6.0 版本后以后被禁止,需要我们手动去管理循环依赖,解决方式如下,将其中某个 bean 的依赖进行延迟初始化。
方式1.使用 @Lazy 注解,将 A 中所依赖的 B 进行延迟加载。
@Component
public class A {
@Lazy
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
方式2.强制走后置set
@Component
public class A {
private B b;
public void setB(@Autowired B b) {
this.b = b;
}
}
@Component
public class B {
@Autowired
private A a;
}
方式3.构造依赖 同样可以使用 @Lazy 解决,被延迟注解所修饰的对象,会生成为一个代理对象。即 A 所注入的 B,将是一个代理对象。
@Component
public class A {
private B b;
public A(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
如上, SpringBoot,版本 2.6.0,循环依赖解决方案。