spring的5种bean状态(Spring的Bean有序吗试试用)

推荐学习
  • 肝了十天半月,献上纯手绘“Spring/Cloud/Boot/MVC”全家桶脑图
  • 双Q合璧:RabbitMQ与RocketMQ,电子版手绘脑图 学习指南 面试等

spring的5种bean状态(Spring的Bean有序吗试试用)(1)

前言

顺序:意思是依次而不乱。顺序在生活的方方面面都显得尤为重要,自然的它对程序执行来说也是至关重要的。有了顺序的保证,我们就能对“结果”做出预期,作为coder的我们对应的也就更能“掌控”自己所写代码,心里也就更加踏实。

顺序固然重要,但是不乏有些场景它是不需要顺序保证的。一般来说:无序的效率会比顺序高,毕竟保证顺序是需要花费资源的(人力、物理、时间…)。本文将主要讨论Spring在实例化Bean时的顺序性,以及我们如何才能“控制”这种顺序呢?

正文

Spring容器载入(实例化)Bean的顺序是不确定的,Spring框架没有约定特定顺序逻辑规范。但是这并不代表Spring没有在这方面做“努力”,下面讲主要以代码示例的形式看效果,最后再从源码的角度分析其原因。

为何需要控制Bean的顺序?

问题的提出可以通过需求场景来驱动,举例如下:

  1. 一个非常典型的场景:事件发布 - 订阅机制。发布者Bean:Publisher,订阅者Bean:Listener。现在有需求:要保证Listener这个Bean能够监听到Publisher发出的所有事件,一个都不能落下,那么这个时候就对Listener这个Bean提出了强制要求:在Publisher初始化之前必须准备好,否则就会错过一些“早期事件”嘛
  2. 该场景在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的实例化顺序能够解释为何它能迟到所有的鱼。但是,但是,但是此解决方案并不推荐,原因如下:

  1. 该方案强依赖于这个规则:同一配置类下,Bean的实例化顺序是按照从上至下的顺序实例化的。一旦你的相关配置处在不同配置类内,此顺序是确定不了的
  2. 这种顺序是由程序员来人工确保的,而非通过结构来固化,因此容错性极低。所以生产上极不推荐这么做
方案二(推荐):使用@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执行...

结论:

  1. @Configuration配置类最优先被初始化,才会继续初始化其里面的@Bean
  2. 若有多个 @Configuration配置类,顺序由你构造AnnotationConfigApplicationContext时传入的顺序为准(若是被scan扫描进去的,则无序)
  3. @Bean方法上加static成为静态方法,并不能提升此Bean的优先级
  4. 主要是因为@Bean的解析,必须是发生在@Configuration配置类被实例化后,因此它并不能提升优先级
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(); } } }

其它不变,再次运行测试程序,控制台输出:

... 配置类Config构造器被执行... 配置类PersonConfig构造器执行... @Bean -> family执行... @Bean -> staticFamily执行... 静态内部配置类StaticInnerClass构造器被执行... @Bean -> staticInnerPerson执行... 内部配置类InnerClass构造器被执行... @Bean -> innerPerson执行... @Bean -> son执行... @Bean -> father执行...

结论:

  • @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这三个注解去控制

通过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

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页