- 肝了十天半月,献上纯手绘“Spring/Cloud/Boot/MVC”全家桶脑图
- 双Q合璧:RabbitMQ与RocketMQ,电子版手绘脑图 学习指南 面试等
前言
顺序:意思是依次而不乱。顺序在生活的方方面面都显得尤为重要,自然的它对程序执行来说也是至关重要的。有了顺序的保证,我们就能对“结果”做出预期,作为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的实例化顺序是按照从上至下的顺序实例化的。一旦你的相关配置处在不同配置类内,此顺序是确定不了的
- 这种顺序是由程序员来人工确保的,而非通过结构来固化,因此容错性极低。所以生产上极不推荐这么做
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执行...
结论:
static使用在Class内部类上
- @Configuration配置类最优先被初始化,才会继续初始化其里面的@Bean
- 若有多个 @Configuration配置类,顺序由你构造AnnotationConfigApplicationContext时传入的顺序为准(若是被scan扫描进去的,则无序)
- @Bean方法上加static成为静态方法,并不能提升此Bean的优先级
- 主要是因为@Bean的解析,必须是发生在@Configuration配置类被实例化后,因此它并不能提升优先级
在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(); } } }
其它不变,再次运行测试程序,控制台输出:
... 配置类Config构造器被执行... 配置类PersonConfig构造器执行... @Bean -> family执行... @Bean -> staticFamily执行... 静态内部配置类StaticInnerClass构造器被执行... @Bean -> staticInnerPerson执行... 内部配置类InnerClass构造器被执行... @Bean -> innerPerson执行... @Bean -> son执行... @Bean -> father执行...
结论:
Spring Boot环境
- @Configuration(外层)配置类的初始化顺序依旧是按照AnnotationConfigApplicationContext的定义顺序来的
- 对于内部类的@Configuration的初始化(不管是静态还是非静态),也依旧是外部的@Configuration完成后才行
- 内部类里的@Bean的优先级均高于外层定义的@Bean,同时可以看到static静态内部类能够提升优先级,它比非静态内部类的优先级还高
- 内部类有限原则它只作用于本@Configuration类,也就是说仅在本主类内提升优先级。另外若出现多个内部类,按照定义顺序执行(static永远高于非static哦)
- 内部类的访问权限无所谓,private都行。
在Spring Boot下会更加关心配置类和@Bean的执行顺序:因为Spring Boot内置了非常多的@Configuration以及@Bean,均是通过扫描的方式“收集”而不能Diy控制,因此它需要提供指定配置类顺序的能力。
控制@Configuration配置类顺序关于Spring Boot下控制@Configuration的顺序,我们会使用@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder这三个注解去控制
通过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来“补救”,我觉得这个“小缺点”已然无伤大雅了。
当然,这并不能算作Spring设计上的缺陷。但是它的底层存储如果使用更为抽象的Collection我觉得是更好的选择,你认为呢?
作者:YourBatman
原文链接:https://blog.csdn.net/f641385712/article/details/105592732
,