在当下 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,又在该配置类中使用它。

spring boot项目怎么看循环引用 springboot怎么解决循环依赖_spring

 当前问题发生于 springBoot 环境 2.6.3,2.5.0 则不会发生该问题。更具体的是哪个版本改的,这里不做深究。

spring boot项目怎么看循环引用 springboot怎么解决循环依赖_spring boot_02

 建议做如下修改,将 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,循环依赖解决方案。