27

static关键字真能提高Bean的优先级吗?答:真能

 3 years ago
source link: http://www.cnblogs.com/yourbatman/p/13304644.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的 专栏 供以免费学习。关注公众号【 BAT的乌托邦 】逐个击破,深入掌握,拒绝浅尝辄止。

目录

Qf6BRzV.png!web

前言

各位小伙伴大家好,我是A哥。关于Spring初始化Bean的顺序问题,是个老生常谈的话题了,结论可总结为一句话: 全局无序,局部有序Spring Bean 整体上是无序的,而现实是大多数情况下我们 真的 无需关心,无序就无序呗,无所谓喽。但是(此处应该有但是哈),我有理由相信,对于有一定从业经验的Javaer来说,或多或少都经历过Bean初始化顺序带来的“困扰”,也许是因为没有对你的功能造成影响,也许可能是你全然“不知情”,所以最终就不了了之~

隐患终归隐患,依照 墨菲定律 来讲,担心的事它总归是会发生的。A哥经常“教唆”程序员要 面向工资编程 ,虽然这价值观有点扭曲,但不可否认很多小伙伴真是这么想的(命中你了没有:smile:),稍加粉饰了而已。话粗理不粗哦,almost所有的Javaer都在用Spring,你凭什么工资比你身边同事的高呢?

Ini26bV.png!web

Spring对Bean的(生命周期)管理是它最为核心的能力,同时也是 很复杂、很难掌握 的一个知识点。现在就可以启动你的工程,有木有这句日志:

"Bean 'xxx' of type [xxxx] is not eligible for getting processed by all BeanPostProcessors" 
	+ "(for example: not eligible for auto-proxying)"

这是一个典型的 Spring Bean过早初始化 问题,搜搜看你日志里是否有此句喽。这句日志是由Spring的 BeanPostProcessorChecker 这个类负责输出,含义为:你的Bean xxx不能被所有的 BeanPostProcessors 处理到(有的生命周期触达不到),提醒你注意。此句日志在低些的版本里是 warn警告 级别,在本文约定的版本里官方把它改为了info级别。

绝大多数情况下,此句日志的输出不会对你的功能造成影响,因此无需搭理。这也是Spring官方为何把它从warn调低为info级别的原因

我在CSDN上写过一篇“Spring Bean过早初始化导致的误伤”的文章, 访问量达近4w

RfERVbb.png!web

从这个数据(访问量)上来看,这件事“并不简单”,遇到此麻烦的小伙伴不在少数且确实难倒了一众人。关于Spring Bean的顺序,全局是不可控的,但是局部上它提供了多种方式来方便使用者提高/降低优先级(比如前面的 使用@AutoConfigureBefore调整配置顺序竟没生效? 这篇文章),本文就聊聊 static关键字对于提供Bean的优先级 的功效。

6ZvYVje.jpg!web

版本约定

本文内容若没做特殊说明,均基于以下版本:

1.8
5.2.2.RELEASE

正文

本文采用从 问题提出-结果分析-解决方案-原理剖析 这4个步骤,层层递进的去感受static关键字在Spring Bean上的魅力~

警告一:来自BeanPostProcessorChecker

这是 最为常见 的一种警告,特别当你的工程使用了 shiro 做鉴权框架的时候。在我记忆中这一年来有N多位小伙伴问过我此问题,可见一斑。

@Configuration
class AppConfig {

    AppConfig() {
        System.out.println("AppConfig init...");
    }

    @Bean
    BeanPostProcessor postProcessor() {
        return new MyBeanPostProcessor();
    }
}

class MyBeanPostProcessor implements BeanPostProcessor {

    MyBeanPostProcessor() {
        System.out.println("MyBeanPostProcessor init...");
    }
}

运行程序,输出结果:

AppConfig init...
2020-05-31 07:40:50.979  INFO 15740 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'appConfig'
	 of type [com.yourbatman.config.AppConfig$$EnhancerBySpringCGLIB$$29b523c8] is not eligible for getting 
	 processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
MyBeanPostProcessor init...
...

结果分析(问题点/冲突点):

  1. AppConfig 优先于 MyBeanPostProcessor 进行实例化
    1. 常识是: MyBeanPostProcessor 作为一个后置处理器理应是先被初始化的,而 AppConfig 仅仅是个普通Bean而已,初始化 理应靠后
  2. 出现了 BeanPostProcessorChecker 日志:表示 AppConfig 这个Bena不能被所有的BeanPostProcessors处理,所以有 可能 会让它“错过”容器对Bean的某些生命周期管理,因此 可能 损失某些能力(比如不能被自动代理),存在 隐患
    1. 但凡只要你工程里出现了 BeanPostProcessorChecker 输出日志,理应都得引起你的注意,因为这属于Spring的警告日志(虽然新版本已下调为了info级别)

说明:这是一个Info日志,并非warn/error级别。绝大多数情况下你确实无需关注,但是如果你是一个容器开发者,建议请务必解决此问题(毕竟貌似大多数中间件开发者都有一定代码洁癖:smile:)

解决方案:static关键字提升优先级

基于上例,我们仅需做如下小改动:

AppConfig:

//@Bean
//BeanPostProcessor postProcessor() {
//    return new MyBeanPostProcessor();
//}

// 方法前面加上static关键字
@Bean
static BeanPostProcessor postProcessor() {
    return new MyBeanPostProcessor();
}

运行程序,结果输出:

MyBeanPostProcessor init...
...
AppConfig init...
...

那个烦人的 BeanPostProcessorChecker 日志就不见了,清爽了很多。同时亦可发现 AppConfig 是在 MyBeanPostProcessor 之后实例化的,这才符合我们所想的“正常”逻辑嘛。

警告二:Configuration配置类增强失败

这个“警告”就比上一个严重得多了,它有 极大的可能 导致你程序错误,并且你还 很难定位 问题所在。

@Configuration
class AppConfig {

    AppConfig() {
        System.out.println("AppConfig init...");
    }

    @Bean
    BeanDefinitionRegistryPostProcessor postProcessor() {
        return new MyBeanDefinitionRegistryPostProcessor();
    }

	///////////////////////////////
    @Bean
    Son son(){
        return new Son();
    }
    @Bean
    Parent parent(){
        return new Parent(son());
    }

}

class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    MyBeanDefinitionRegistryPostProcessor() {
        System.out.println("MyBeanDefinitionRegistryPostProcessor init...");
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}

运行程序,结果输出:

AppConfig init...
MyBeanDefinitionRegistryPostProcessor init...
2020-05-31 07:59:06.363  INFO 37512 --- [           main] o.s.c.a.ConfigurationClassPostProcessor  : Cannot enhance
	 @Configuration bean definition 'appConfig' since its singleton instance has been created too early. The typical
	 cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring
	 such methods as 'static'.
...
son init...hashCode() = 1300528434
son init...hashCode() = 1598434875
Parent init...

结果分析(问题点/冲突点):

  1. AppConfig竟然比MyBeanDefinitionRegistryPostProcessor的初始化时机还早,这本就不合理
  2. ConfigurationClassPostProcessor 的日志中可看到: AppConfig配置类enhance增强失败
  3. Son对象竟然被创建了 两个 不同的实例,这将会直接导致功能性错误

这三步结果环环相扣,因为1导致了2的增强失败,因为2的增强失败导致了3的创建多个实例,真可谓 一步错,步步错 。需要注意的是:这里ConfigurationClassPostProcessor输出的依旧是info日志(我个人认为,Spring把这个输出调整为warn级别是更为合理的,因为它影响较大)。

说明:对这个结果的理解基于对Spring配置类的理解,因此强烈建议你进我公众号参阅那个可能是写的最全、最好的Spring配置类专栏学习(文章不多,6篇足矣)

源码处解释:

ConfigurationClassPostProcessor:

// 对Full模式的配置类尝试使用CGLIB字节码提升
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
	...
	// 对Full模式的配置类有个判断/校验
	if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
		if (!(beanDef instanceof AbstractBeanDefinition)) {
			throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
					beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
		}

		// 若判断发现此时该配置类已经是个单例Bean了(说明已初始化完成)
		// 那就不再做处理,并且输出警告日志告知使用者(虽然是info日志)
		else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
			logger.info("Cannot enhance @Configuration bean definition '" + beanName +
					"' since its singleton instance has been created too early. The typical cause " +
					"is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
					"return type: Consider declaring such methods as 'static'.");
		}
		configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
	}
	...
}

由于配置类增强是在 BeanFactoryPostProcessor#postProcessBeanFactory() 声明周期阶段去做的,而 BeanDefinitionRegistryPostProcessor 它会优先于该步骤完成实例化(其实主要是优先级比 BeanFactoryPostProcessor 高),从而 间接带动 AppConfig提前初始化导致了问题,这便是根本原因所在。

提问点:本处使用了个自定义的 BeanDefinitionRegistryPostProcessor 模拟了效果,那如果你是使用的 BeanFactoryPostProcessor 能出来这个效果吗???答案是 不能的 ,具体原因留给读者思考,可参考: PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors 这段流程辅助理解。

解决方案:static关键字提升优先级

来吧,继续使用static关键字改造一下:

AppConfig:

//@Bean
//BeanDefinitionRegistryPostProcessor postProcessor() {
//    return new MyBeanDefinitionRegistryPostProcessor();
//}

@Bean
static BeanDefinitionRegistryPostProcessor postProcessor() {
    return new MyBeanDefinitionRegistryPostProcessor();
}

运行程序,结果输出:

MyBeanDefinitionRegistryPostProcessor init...
...
AppConfig init...
son init...hashCode() = 2090289474
Parent init...
...

完美。

警告三:非静态@Bean方法导致@Autowired等注解失效

@Configuration
class AppConfig {

    @Autowired
    private Parent parent;
    @PostConstruct
    void init() {
        System.out.println("AppConfig.parent = " + parent);
    }


    AppConfig() {
        System.out.println("AppConfig init...");
    }

    @Bean
    BeanFactoryPostProcessor postProcessor() {
        return new MyBeanFactoryPostProcessor();
    }

    @Bean
    Son son() {
        return new Son();
    }
    @Bean
    Parent parent() {
        return new Parent(son());
    }
}

class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    MyBeanFactoryPostProcessor() {
        System.out.println("MyBeanFactoryPostProcessor init...");
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

运行程序,结果输出:

AppConfig init...
2020-05-31 08:28:06.550  INFO 1464 --- [           main] o.s.c.a.ConfigurationClassEnhancer       : @Bean method
	 AppConfig.postProcessor is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor
	 interface. This will result in a failure to process annotations such as @Autowired, @Resource and 
	 @PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to 
	 this method to avoid these container lifecycle issues; see @Bean javadoc for complete details.
MyBeanFactoryPostProcessor init...
...
son init...hashCode() = 882706486
Parent init...

结果分析(问题点/冲突点):

  1. AppConfig提前于 MyBeanFactoryPostProcessor 初始化
  2. @Autowired/@PostConstruct 等注解没有生效, 这个问题很大

需要强调的是:此时的AppConfig是被enhance增强成功了的,这样才有可能进入到 BeanMethodInterceptor 拦截里面,才有可能输出这句日志(该拦截器会拦截Full模式配置列的所有的@Bean方法的执行)

这句日志由 ConfigurationClassEnhancer.BeanMethodInterceptor 输出,含义为:你的@Bean标注的方法是非static的并且返回了一个 BeanFactoryPostProcessor 类型的实例,这就导致了 配置类 里面的 @Autowired, @Resource,@PostConstruct 等注解都将得不到解析,这是比较危险的(所以其实这个日志调整为warn级别也是阔仪的)。

小细节:为毛日志看起来是ConfigurationClassEnhancer这个类输出的呢?这是因为 BeanMethodInterceptor 是它的静态内部类,和它 共用的 一个logger

源码处解释:

ConfigurationClassEnhancer.BeanMethodInterceptor:

	if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
		if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
			logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
							"assignable to Spring's BeanFactoryPostProcessor interface. This will " +
							"result in a failure to process annotations such as @Autowired, " +
							"@Resource and @PostConstruct within the method's declaring " +
							"@Configuration class. Add the 'static' modifier to this method to avoid " +
							"these container lifecycle issues; see @Bean javadoc for complete details.",
					beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
		}
		return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
	}

解释为:如果当前正在执行的@Bean方法(铁定不是static,因为静态方法它也拦截不到嘛)返回类型是 BeanFactoryPostProcessor 类型,那就输出此警告日志来提醒使用者要当心。

解决方案:static关键字提升优先级

AppConfig:

//@Bean
//BeanFactoryPostProcessor postProcessor() {
//    return new MyBeanFactoryPostProcessor();
//}

@Bean
static BeanFactoryPostProcessor postProcessor() {
    return new MyBeanFactoryPostProcessor();
}

运行程序,结果输出:

MyBeanFactoryPostProcessor init...
AppConfig init...
son init...hashCode() = 1906549136
Parent init...
// @PostConstruct注解生效喽
AppConfig.parent = com.yourbatman.bean.Parent@baf1bb3
...

世界一下子又清爽了有木有。

原因总结

以上三个case是有共同点的,粗略的讲导致它们的原因甚至是同一个: AppConfig这个Bean被过早初始化 。然而我们的解决方案似乎也是同一个: 使用static提升Bean的优先级

那么为何AppConfig会被提前初始化呢?为何使用static关键字就没有问题了呢?根本原因可提前剧透:static静态方法属于类,执行静态方法时并不需要初始化所在类的实例;而实例方法属于实例,执行它时必须先初始化所在类的实例。听起来是不是非常的简单,JavaSE的东西嘛,当然只知晓到这个层次肯定是远远不够的,限于篇幅原因,关于Spring是如何处理的 源码级别的分析 我放在了下篇文章,请别走开哟~

static静态方法一定优先执行吗?

看完本文,有些小伙伴就忍不住跃跃欲试了,甚至很武断的得出结论: static标注的@Bean方法优先级更高 ,其实这是错误的,比如你看如下示例:

@Configuration
class AppConfig2 {

    AppConfig2(){
        System.out.println("AppConfig2 init...");
    }

    @Bean
    Son son() {
        return new Son();
    }
    @Bean
    Daughter daughter() {
        return new Daughter();
    }
    @Bean
    Parent Parent() {
        return new Parent();
    }
}

运行程序,结果输出:

AppConfig2 init...
son init...
Daughter init...
Parent init...

这时候你想让Parent在Son之前初始化,因此你想着在用static关键字来提升优先级,这么做:

AppConfig2:

//@Bean
//Parent Parent() {
//    return new Parent();
//}
@Bean
static Parent Parent() {
    return new Parent();
}

结果:你徒劳了,static貌似并没有生效,怎么回事?

原因浅析

为了满足你的好奇心,这里给个浅析,道出关键因素。我们知道@Bean方法(不管是静态方法还是实例方法)最终都会被封装进 ConfigurationClass 实例里面,使用 Set<BeanMethod> beanMethods 存储着,关键点在于它是个 LinkedHashSet 所以是有序的(存放顺序),而存入的顺序底层是由 clazz.getDeclaredMethods() 来决定的,由此可知@Bean方法执行顺序 和有无static没有半毛钱关系

ZZJ3UnF.png!web

说明: clazz.getDeclaredMethods() 得到的是Method[]数组,是有序的。这个顺序由字节码(定义顺序)来保证:先定义,先服务。

由此可见,static并不是 真正意义 上的提高Bean优先级,对于如上你的需求case,你可以使用 @DependsOn 注解来保证,它也是和Bean顺序息息相关的一个注解,在本专栏后续文章中将会详细讲到。

所以关于@Bean方法的执行顺序的正确结论应该是:在 同一配置类 内,在无其它“干扰”情况下(无 @DependsOn、@Lazy等注解 ),@Bean方法的执行顺序遵从的是定义顺序(后置处理器类型除外)。

小提问:如果是垮@Configuration配置类的情况,顺序如何界定呢?那么这就不是同一层级的问题了,首先考虑的应该是@Configuration配置类的顺序问题,前面有文章提到过配置类是支持 有限的 的@Order注解排序的,具体分析请依旧保持关注A哥后续文章详解哈...

static关键字使用注意事项

在同一个 @Configuration 配置类内,对static关键字的使用做出如下说明,供以参考:

  1. 对于普通类型(非后置处理器类型)的@Bean方法,使用static关键字并不能改变顺序(按照方法定义顺序执行),所以别指望它
  2. static关键字一般有且仅用于@Bean方法返回为 BeanPostProcessorBeanFactoryPostProcessor 等类型的方法,并且建议此种方法 请务必使用static修饰 ,否则容易导致隐患,埋雷

static关键字不要滥用(其实任何关键字皆勿乱用),在同一配置类内, 与其说它是提升了Bean的优先级,倒不如说它让@Bean方法静态化从而不再需要依赖所在类的实例即可独立运行 。另外我们知道,static关键还可以修饰(内部)类,那么如果放在类上它又是什么表现呢?同样的,你先思考,下篇文章我们接着聊~

说明:使用static修饰Class类在 Spring Boot 自动配置类里特别特别常见,所以掌握起来很具价值

思考题:

今天的思考题比较简单:为何文首三种case的警告信息都是info级别呢?是否有级别过低之嫌?

总结

本文还是蛮干的哈,不出意外它能够帮你解决你工程中的某些问题,排除掉一些隐患,毕竟墨菲定律被验证了你担心的事它总会发生,防患于未然才能把自己置于安全高地嘛。

你可能诧异,A哥竟能把static关键字在Spring中的应用都能写出个专栏出来,是的,这不是就是本公众号的定位么 ,小而美和拒绝浅尝辄止嘛。对于一些知识(比如本文的static关键字的使用)我并不推崇强行记忆,因为那真的很容易忘,快速使用可以简单记记,但真想记得牢(甚至成为永久记忆),那必须得去深水区看看。来吧,下文将授之以渔~

很多小伙伴去强行记忆Spring Boot支持的那17种外部化配置,此时你应该问自己:现在你可能记得,一周以后呢?一个月以后呢?所以你需要另辟蹊径,那就持续关注我吧:smile:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK