Spring的Bean有序吗?试试用@DependsOn或static来提高优先级

永远不要把自己的全部时间给公司,人生需要积累,技术也一样,代码也是。

前言

顺序:意思是依次而不乱。顺序在生活的方方面面都显得尤为重要,自然的它对程序执行来说也是至关重要的。有了顺序的保证,我们就能对“结果”做出预期,作为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
    1. 若有多个 @Configuration配置类,顺序由你构造AnnotationConfigApplicationContext时传入的顺序为准(若是被scan扫描进去的,则无序)
  2. @Bean方法上加static成为静态方法,并不能提升此Bean的优先级
    1. 主要是因为@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执行...

结论:

  1. @Configuration(外层)配置类的初始化顺序依旧是按照AnnotationConfigApplicationContext的定义顺序来的
    1. 对于内部类@Configuration的初始化(不管是静态还是非静态),也依旧是外部的@Configuration完成后才行
  2. 内部类里的@Bean的优先级均高于外层定义的@Bean,同时可以看到static静态内部类能够提升优先级,它比非静态内部类的优先级还高
  3. 内部类有限原则它只作用于本@Configuration类,也就是说仅在本主类内提升优先级。另外若出现多个内部类,按照定义顺序执行(static永远高于非static哦)
  4. 内部类的访问权限无所谓,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提升优先级的区别

其实把他俩放在一起比较其实蛮牵强的,根本不是同一回事嘛。但是在提升优先级方面,此处絮叨两句:

  1. @DependsOn强调的是Bean与Bean之间的依赖关系。如:A @DependsOn B表示,只有当B初始化完成了才会去初始化A。这里所谓的Bean可以是任何Bean:包括@Bean、@Component、@Configuration等一切形式
  2. static它主要运用在@Configuration配置文件来提升优先级,这种优先级体现在:内部类里的@Bean比外部类会先加载,static静态内部类的@Bean又会比普通内部类的@Bean先加载

总结

本文主要讲解了Spring、Spring Boot中对配置文件以及Bean的加载顺序问题,虽说我们并不能绝对的控制Bean的顺序,但我们能采取一定的措施,如使用@DependsOnstatic来提高某些Bean的优先级或者相对顺序,这便也能解决我们的需求。在实际使用中,我们的确并不需要控制每个Bean的顺序,而只需操控其相对顺序即可。

有的人说不能控制Bean的顺序是Spring容器在设计时疏忽的一点(究其原因是底层使用了Set的结构,因此无法保证顺序),我也在一定程度上表示赞同。但是它提供了形如@Order、@DependsOn、static来“补救”,我觉得这个“小缺点”已然无伤大雅了。

当然,这并不能算作Spring设计上的缺陷。但是它的底层存储如果使用更为抽象的Collection我觉得是更好的选择,你认为呢?


关注A哥

Author A哥(YourBatman)
个人站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活跃平台
公众号 BAT的乌托邦(ID:BAT-utopia)
知识星球 BAT的乌托邦
每日文章推荐 每日文章推荐

BAT的乌托邦

往期精选

YourBatman CSDN认证博客专家 博客专家 专栏创作者 BAT的乌托邦
也许当我老了,也一样写代码。不为别的,只为爱好
公众号:BAT的乌托邦
亦可在这里和我交流:https://www.yourbatman.cn
©️2020 CSDN 皮肤主题: 代码科技 设计师: Amelia_0503 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值