spring bean加载顺序问题
【bean循环依赖解决参考思路】
前言
顺序:意思是依次而不乱。顺序在生活的方方面面都显得尤为重要,自然的它对
程序执行
来说也是至关重要的。有了顺序的保证,我们就能对“结果”做出预期,作为coder的我们对应的也就更能“掌控”自己所写代码,心里也就更加踏实。
顺序固然重要,但是不乏有些场景它是不需要顺序保证的。
一般来说
:无序的效率会比顺序高,毕竟保证顺序是需要花费资源的(人力、物理、时间…)。本文将主要讨论
Spring在实例化Bean时的顺序性
,以及我们如何才能“控制”这种顺序呢?
正文
Spring
容器
载入(实例化)Bean的顺序是不确定的,Spring框架
没有约定
特定顺序逻辑规范。但是这并不代表Spring没有在这方面做“努力”,下面讲主要以代码示例的形式看效果,最后再从源码的角度分析其原因。
为何需要控制Bean的顺序?
问题的提出可以通过需求场景来驱动,举例如下:
-
一个非常典型的场景:事件发布 – 订阅机制。发布者Bean:
Publisher
,订阅者Bean:
Listener
。现在有需求:要保证
Listener
这个Bean能够监听到
Publisher
发出的所有事件,一个都不能落下,那么这个时候就对
Listener
这个Bean提出了强制要求:在
Publisher
初始化之前必须准备好,否则就会错过一些“早期事件”嘛 -
该场景在
Spring Boot
的自动配置中极为常见:此处我拿Feign和Hystrix的整合举例。Feign若不和Hystrix整合使用的是
feign.Feign.Builder
构建器,若整合使用的是
feign.hystrix.HystrixFeign
构建器,
因此这里存在先后顺序
:必须先判断是否能构建起
HystrixFeign
实例(类路径下是否有相关类),再去考虑
原生Builder
,这中case也就对顺序有强依赖了。
关于顺序的控制,本文区分出传统Spring环境下和Spring Boot环境下的不同处理(请以前者为主)。
传统Spring环境
场景一示例:
这里我用JDK原生的
Observable/Observer
机制来写出观察者模式代码(结合Spring):
主人(事件发布者):
// 主人:最终会有小动物观察主人
// 主人有个能力:放鱼
public class Master extends Observable {
public String name;
private Master(String name) {
this.name = name;
}
// 给鱼:然后通知所有的观察者过来吃鱼。这样所有观察的猫都会过来了
public void giveFish() {
System.out.println(name + "主人放了一条鱼,通知猫过来吃~~~~~~");
setChanged(); // 这个一定不能忘
notifyObservers();
}
// 单例
private static final Master MASTER = new Master("YoutBatman");
public static Master getMaster() {
return MASTER;
}
}
// 它作为Master的代理,把它放进容器内,而非Master本身
public class MasterBean implements InitializingBean {
// 初始化完成后,立马放一条鱼
@Override
public void afterPropertiesSet() throws Exception {
Master.getMaster().giveFish();
}
}
猫(观察者):
// 观察者:它会观察主人,只要放鱼了它就会去吃(消费)
public class Cat implements Observer {
public String name;
public Cat(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
String masterName = o.toString();
// 因为该观察者接口没有泛型 所以只能强转
if (o instanceof Master) {
masterName = ((Master) o).name;
}
System.out.println(name + "吃了主人" + masterName + "放的鱼");
}
}
主人 + 猫的关系绑定上(通过Spring配置):本文放两只猫
@Configuration(proxyBeanMethods = false)
public class Config {
@Bean
public MasterBean master() {
return new MasterBean();
}
@Bean
public Cat tom() {
Cat tom = new Cat("Tom");
Master.getMaster().addObserver(tom);
return tom;
}
@Bean
public Cat cc() {
Cat cc = new Cat("Cc");
Master.getMaster().addObserver(cc);
return cc;
}
}
书写测试程序:
public static void main(String[] args) {
new AnnotationConfigApplicationContext(Config.class);
Master.getMaster().giveFish();
Master.getMaster().giveFish();
}
运行程序,控制台输出:
...
09:36:35.096 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
09:36:35.103 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'master'
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
09:36:35.106 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'tom'
09:36:35.107 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cc'
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Cc吃了主人YoutBatman放的鱼
Tom吃了主人YoutBatman放的鱼
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Cc吃了主人YoutBatman放的鱼
Tom吃了主人YoutBatman放的鱼
从debug日志中很明显的看到这些Bean的初始化顺序为:
config -> master -> tom -> cc
,所以当master初始化完毕后放出鱼时,两只猫都没有监听到,所以
错失了首次放的鱼
,这也就是错失某些事件的例子,在生产上很多时候是不能容忍的,需要解决。
解决方案
针对此种case,我们的诉求是希望无论如何猫兄都能监听到主人放鱼的动作,从而“吃到所有的鱼”。那么此处我给出三种方案供你参考:
方案一(不推荐):改变@Bean的定义顺序
把上面的
Config.java
配置文件改为如下顺序:
@Configuration(proxyBeanMethods = false)
public class Config {
@Bean
public Cat tom() {
Cat tom = new Cat("Tom");
Master.getMaster().addObserver(tom);
return tom;
}
@Bean
public Cat cc() {
Cat cc = new Cat("Cc");
Master.getMaster().addObserver(cc);
return cc;
}
@Bean
public MasterBean master() {
return new MasterBean();
}
}
其它均不变,再次运行程序,控制台输出:
...
09:44:03.987 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
09:44:03.994 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'tom'
09:44:04.000 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cc'
09:44:04.000 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'master'
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Cc吃了主人YoutBatman放的鱼
Tom吃了主人YoutBatman放的鱼
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Cc吃了主人YoutBatman放的鱼
Tom吃了主人YoutBatman放的鱼
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Cc吃了主人YoutBatman放的鱼
Tom吃了主人YoutBatman放的鱼
问题解决:猫兄吃到了所有的鱼,从debug日志中Bean的实例化顺序能够解释为何它能迟到所有的鱼。但是,但是,但是此解决方案并不推荐,原因如下:
-
该方案强依赖于这个规则:
同一配置类下
,Bean的实例化顺序是按照从上至下的顺序实例化的。一旦你的相关配置处在不同配置类内,此顺序是确定不了的 -
这种顺序是由程序员来
人工确保
的,而非通过结构来固化,因此容错性极低。所以生产上极不推荐这么做
方案二(推荐):使用@DependsOn
Spring提供了一个
@DependsOn
注解,能够解决这类问题。这个场景的核心思想是:猫(监听者)必须确保在主人(事件发送者)放鱼(发送事件动作)之前完成实例化且注册监听,这样才不会错过每一条鱼。所以我们可以这么做(依旧基于原Config.java文件做出修改):
@Configuration(proxyBeanMethods = false)
public class Config {
// @DependsOn // 若里面不写值,该注解无效。但若写了值,请确保里面的Bean都有,否则报错
@DependsOn({"cc", "tom"})
@Bean
public MasterBean master() {
return new MasterBean();
}
@Bean
public Cat tom() {
Cat tom = new Cat("Tom");
Master.getMaster().addObserver(tom);
return tom;
}
@Bean
public Cat cc() {
Cat cc = new Cat("Cc");
Master.getMaster().addObserver(cc);
return cc;
}
}
其它不变,再次运行程序,控制台输出:
...
10:04:10.729 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
10:04:10.736 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cc'
10:04:10.741 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'tom'
10:04:10.741 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'master'
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Tom吃了主人YoutBatman放的鱼
Cc吃了主人YoutBatman放的鱼
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Tom吃了主人YoutBatman放的鱼
Cc吃了主人YoutBatman放的鱼
YoutBatman主人放了一条鱼,通知猫过来吃~~~~~~
Tom吃了主人YoutBatman放的鱼
Cc吃了主人YoutBatman放的鱼
完美。这种处理方式是被推荐的方式,它能
显示的
控制Bean的依赖关系,而不受到其它影响,是值得信赖的使用方式。
说明:在“编程界”有个设计原则:显示的指出往往比隐式的更好,更稳定和更具表达力
方案三(不推荐):使用@Lazy
使用
@Lazy
只是一种曲线的解决方案,有些case它并不适合,因此并不推荐。
场景二示例:
这种场景在纯Spring环境下我们
几乎遇不见
,缘由是在Spring下所有的配置文件都是我们手动确定和编写,所以“哪些能写、哪些不能写,哪些在前,哪些在后”均是确定的,由我们程序员自行控制。该场景在Spring Boot场景下被大量用到,下面会举例说明。
当然,即使到了Spring Boot下,此部分初始化原理依旧是
Spring Framwork
的,因此这里也不闲着,通过代码示例来展示其加载顺序,核心便是介绍
static
关键字的使用。
使用static提升Bean的优先级
static
代表静态,标注在类上表示该类是静态(内部)类,标注在方法上表示该方法是属于类的静态方法(不需要实例化即可调用),“看起来”可以是可以提升优先级的,那么实际如何呢?不能臆断,且看下面示例
static使用在@Bean方法上
准备两个配置类:
@Configuration(proxyBeanMethods = false)
public class PersonConfig {
public PersonConfig() {
System.out.println("配置类PersonConfig构造器执行...");
}
@Bean
public Person son() {
System.out.println("@Bean -> son执行...");
return new Person("YourBatman-son", 18);
}
@Bean
public Person father() {
System.out.println("@Bean -> father执行...");
return new Person("YourBatman", 48);
}
}
@Configuration(proxyBeanMethods = false)
public class Config {
public Config() {
System.out.println("配置类Config构造器被执行...");
}
@Bean
public Family family() {
System.out.println("@Bean -> family执行...");
return new Family();
}
@Bean
public static Family staticFamily() {
System.out.println("@Bean -> staticFamily执行...");
return new Family();
}
}
书写测试程序:
public static void main(String[] args) {
new AnnotationConfigApplicationContext(Config.class, PersonConfig.class);
}
运行程序,控制台打印:
配置类Config构造器被执行...
配置类PersonConfig构造器执行...
@Bean -> family执行...
@Bean -> staticFamily执行...
@Bean -> son执行...
@Bean -> father执行...
结论:
-
@Configuration
配置类最优先被初始化,才会继续初始化其里面的@Bean-
若有多个
@Configuration
配置类,顺序由你构造
AnnotationConfigApplicationContext
时传入的顺序为准(若是被scan扫描进去的,则无序)
-
若有多个
-
@Bean方法上加static成为静态方法,并
不能
提升此Bean的优先级-
主要是因为@Bean的解析,必须是发生在
@Configuration
配置类被实例化后,因此它并不能提升优先级
-
主要是因为@Bean的解析,必须是发生在
static使用在Class内部类上
在
PersonConfig
里增加一个静态内部类:
@Configuration(proxyBeanMethods = false)
public class PersonConfig {
... // 同上
// 非静态内部类
@Configuration(proxyBeanMethods = false)
private class InnerClass {
public InnerClass() {
System.out.println("内部配置类InnerClass构造器被执行...");
}
@Bean
public Person innerPerson() {
System.out.println("@Bean -> innerPerson执行...");
return new Person();
}
}
// static静态内部类
@Configuration(proxyBeanMethods = false)
private static class StaticInnerClass {
public StaticInnerClass() {
System.out.println("静态内部配置类StaticInnerClass构造器被执行...");
}
@Bean
public Person staticInnerPerson() {
System.out.println("@Bean -> staticInnerPerson执行...");
return new Person();
}
}
}
其它不变,再次运行测试程序,控制台输出:
结论:
-
@Configuration
(外层)配置类的初始化顺序依旧是按照
AnnotationConfigApplicationContext
的定义顺序来的-
对于
内部类
的
@Configuration
的初始化(不管是静态还是非静态),也依旧是外部的
@Configuration
完成后才行
-
对于
-
内部类里的@Bean的优先级均高于外层定义的@Bean,同时可以看到
static静态内部类能够提升优先级
,它比非静态内部类的优先级还高 -
内部类有限原则它只作用于本
@Configuration
类,也就是说仅在本主类内提升优先级。另外若出现多个内部类,按照定义顺序执行(static永远高于非static哦) - 内部类的访问权限无所谓,private都行。
Spring Boot环境
在
Spring Boot
下会更加关心配置类和@Bean的执行顺序:因为Spring Boot内置了非常多的
@Configuration
以及@Bean,均是通过扫描的方式“收集”而不能Diy控制,因此它需要提供指定配置类顺序的能力。
控制@Configuration配置类顺序
关于Spring Boot下控制@Configuration的顺序,我们会使用
@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder
这三个注解去控制,关于它们的
正确使用姿势
,请参阅:
你了解Spring Boot的自动配置吗?为何我的@AutoConfigureBefore注解不生效?
通过static提升优先级的示例
在Spring Boot的自动配置里,有非常多的通过static提升优先级的case,这里我找了个熟悉的例子进行说明:
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
// 当Classpath里存在HystrixCommand、HystrixFeign等类时,就自动和Hystrix集成
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
}
这是一个典型案例:当然类路径存在Hystrix时,自动使用带有熔断功能的
HystrixFeign.builder
构建器,否则使用的默认的
Feign.builder
构建器。此处利用的就是内部类具有更高优先级,因此可以先去执行判断~
控制@Bean顺序
同Spring Framwork。
@DependsOn和static提升优先级的区别
其实把他俩放在一起比较其实蛮牵强的,根本不是同一回事嘛。但是在提升优先级方面,此处絮叨两句:
-
@DependsOn
强调的是Bean与Bean之间的依赖关系。如:A @DependsOn B表示,只有当B初始化完成了才会去初始化A。这里所谓的Bean可以是任何Bean:包括@Bean、@Component、@Configuration等一切形式 -
static
它主要运用在
@Configuration
配置文件
内
来提升优先级,这种优先级体现在:内部类里的@Bean比外部类会先加载,static静态内部类的@Bean又会比普通内部类的@Bean先加载
总结
本文主要讲解了Spring、Spring Boot中对配置文件以及Bean的加载顺序问题,虽说我们并不能绝对的控制Bean的顺序,但我们能采取一定的措施,如使用
@DependsOn
或
static
来提高某些Bean的优先级或者相对顺序,这便也能解决我们的需求。在实际使用中,我们的确并不需要控制每个Bean的顺序,而只需操控其相对顺序即可。
有的人说不能控制Bean的顺序是Spring容器在设计时疏忽的一点(究其原因是底层使用了Set的结构,因此无法保证顺序),我也在一定程度上表示赞同。但是它提供了形如
@Order、@DependsOn、static
来“补救”,我觉得这个“小缺点”已然无伤大雅了。