static关键字真能提高Bean的优先级吗?答:真能
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。
前言
各位小伙伴大家好,我是A哥。关于Spring初始化Bean的顺序问题,是个老生常谈的话题了,结论可总结为一句话:全局无序,局部有序。Spring Bean
整体上是无序的,而现实是大多数情况下我们真的无需关心,无序就无序呗,无所谓喽。但是(此处应该有但是哈),我有理由相信,对于有一定从业经验的Javaer来说,或多或少都经历过Bean初始化顺序带来的“困扰”,也许是因为没有对你的功能造成影响,也许可能是你全然“不知情”,所以最终就不了了之~
隐患终归隐患,依照墨菲定律来讲,担心的事它总归是会发生的。A哥经常“教唆”程序员要面向工资编程,虽然这价值观有点扭曲,但不可否认很多小伙伴真是这么想的(命中你了没有),稍加粉饰了而已。话粗理不粗哦,almost所有的Javaer都在用Spring,你凭什么工资比你身边同事的高呢?
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:
从这个数据(访问量)上来看,这件事“并不简单”,遇到此麻烦的小伙伴不在少数且确实难倒了一众人。关于Spring Bean的顺序,全局是不可控的,但是局部上它提供了多种方式来方便使用者提高/降低优先级(比如前面的使用@AutoConfigureBefore调整配置顺序竟没生效?这篇文章),本文就聊聊static关键字对于提供Bean的优先级的功效。
版本约定
本文内容若没做特殊说明,均基于以下版本:
- JDK:
1.8
- Spring Framework:
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...
...
结果分析(问题点/冲突点):
AppConfig
优先于MyBeanPostProcessor
进行实例化- 常识是:
MyBeanPostProcessor
作为一个后置处理器理应是先被初始化的,而AppConfig
仅仅是个普通Bean而已,初始化理应靠后
- 常识是:
- 出现了
BeanPostProcessorChecker
日志:表示AppConfig
这个Bena不能被所有的BeanPostProcessors处理,所以有可能会让它“错过”容器对Bean的某些生命周期管理,因此可能损失某些能力(比如不能被自动代理),存在隐患- 但凡只要你工程里出现了
BeanPostProcessorChecker
输出日志,理应都得引起你的注意,因为这属于Spring的警告日志(虽然新版本已下调为了info级别)
- 但凡只要你工程里出现了
说明:这是一个Info日志,并非warn/error级别。绝大多数情况下你确实无需关注,但是如果你是一个容器开发者,建议请务必解决此问题(毕竟貌似大多数中间件开发者都有一定代码洁癖)
解决方案: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...
结果分析(问题点/冲突点):
- AppConfig竟然比MyBeanDefinitionRegistryPostProcessor的初始化时机还早,这本就不合理
- 从
ConfigurationClassPostProcessor
的日志中可看到:AppConfig配置类enhance增强失败 - 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...
结果分析(问题点/冲突点):
- AppConfig提前于
MyBeanFactoryPostProcessor
初始化 @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没有半毛钱关系。
说明:
clazz.getDeclaredMethods()
得到的是Method[]数组,是有序的。这个顺序由字节码(定义顺序)来保证:先定义,先服务。
由此可见,static并不是真正意义上的提高Bean优先级,对于如上你的需求case,你可以使用@DependsOn
注解来保证,它也是和Bean顺序息息相关的一个注解,在本专栏后续文章中将会详细讲到。
所以关于@Bean方法的执行顺序的正确结论应该是:在同一配置类内,在无其它“干扰”情况下(无@DependsOn、@Lazy等注解
),@Bean方法的执行顺序遵从的是定义顺序(后置处理器类型除外)。
小提问:如果是垮@Configuration配置类的情况,顺序如何界定呢?那么这就不是同一层级的问题了,首先考虑的应该是@Configuration配置类的顺序问题,前面有文章提到过配置类是支持有限的的@Order注解排序的,具体分析请依旧保持关注A哥后续文章详解哈...
static关键字使用注意事项
在同一个@Configuration
配置类内,对static关键字的使用做出如下说明,供以参考:
- 对于普通类型(非后置处理器类型)的@Bean方法,使用static关键字并不能改变顺序(按照方法定义顺序执行),所以别指望它
- static关键字一般有且仅用于@Bean方法返回为
BeanPostProcessor
、BeanFactoryPostProcessor
等类型的方法,并且建议此种方法请务必使用static修饰,否则容易导致隐患,埋雷
static关键字不要滥用(其实任何关键字皆勿乱用),在同一配置类内,与其说它是提升了Bean的优先级,倒不如说它让@Bean方法静态化从而不再需要依赖所在类的实例即可独立运行。另外我们知道,static关键还可以修饰(内部)类,那么如果放在类上它又是什么表现呢?同样的,你先思考,下篇文章我们接着聊~
说明:使用static修饰Class类在Spring Boot自动配置类里特别特别常见,所以掌握起来很具价值
思考题:
今天的思考题比较简单:为何文首三种case的警告信息都是info级别呢?是否有级别过低之嫌?
总结
本文还是蛮干的哈,不出意外它能够帮你解决你工程中的某些问题,排除掉一些隐患,毕竟墨菲定律被验证了你担心的事它总会发生,防患于未然才能把自己置于安全高地嘛。
你可能诧异,A哥竟能把static关键字在Spring中的应用都能写出个专栏出来,是的,这不是就是本公众号的定位么 ,小而美和拒绝浅尝辄止嘛。对于一些知识(比如本文的static关键字的使用)我并不推崇强行记忆,因为那真的很容易忘,快速使用可以简单记记,但真想记得牢(甚至成为永久记忆),那必须得去深水区看看。来吧,下文将授之以渔~
很多小伙伴去强行记忆Spring Boot支持的那17种外部化配置,此时你应该问自己:现在你可能记得,一周以后呢?一个月以后呢?所以你需要另辟蹊径,那就持续关注我吧
static关键字真能提高Bean的优先级吗?答:真能的更多相关文章
- static关键字有何魔法?竟让Spring Boot搞出那么多静态内部类
生命太短暂,不要去做一些根本没有人想要的东西.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习 ...
- static关键字,引发的spring普通类获取spring的bean的思考
在c++和java中static关键字用于修饰静态成员变量和成员函数 举例一个普通的javabean class AA { int a; static int b; geta/seta;//此处省略g ...
- static 关键字
static对象如果出现在类中,那么该对象即使从未被使用到,它也会被构造以及析构.而函数中的static对象,如果该函数从未被调用,这个对象也就绝不会诞生,但是在函数每次被调用时检查对象是否需要诞生. ...
- 理解Java中的final和static关键字
回顾这两个关键字前,先考虑一个问题: Static变量存储在JVM中的位置,或者说static变量是如何被加载的? JVM会把类的静态方法和静态变量在类加载的过程中读入方法区(Method Area) ...
- 细说static关键字及其应用
场景 先看段代码,考虑以下场景,其运行结果是什么? public class Test { static int i = 8; public void printI() { int i = 88; S ...
- 关于java的static关键字
通常来说,当你创建类时,就是在描述那个类的对象的外观与行为.除非你用new创建那个类的对象,否则,你实际上并未获得任何东西.当你用new来创建对象时,数据存储空间才被分配,其方法才供外界调用. 但是有 ...
- 【转载】Static 关键字的作用
原始日期:2016-07-16 17:53 一 普通的static关键字 1. 静态全局变量 在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量.我们先举一个静态全局变 ...
- C/C++中static关键字的用法
1.什么是static? static 是C/C++中很常用的修饰符,它被用来控制变量的存储方式和可见性. 1.1static的引入 我们知道在函数内部定义的变量,当程序执行到它的定义处时,编译器为它 ...
- Java:final、static关键字 详解+两者结合使用
一 final关键字 1) 关于final的重要知识点 final关键字可以用于成员变量.本地变量.方法以及类. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误. ...
随机推荐
- c常用函数-strlen
strlen 返回字符串长度 Action() { char test[] = "yyyHHHJJJoo"; unsigned int a; a = strlen(test); l ...
- Elasticsearch原理入门
这是一篇拼接贴,我是缝合怪 项目中用到了es,使用方法是挺简单的,封装了基本api以后,把查询条件封装一下传给client执行就可,但是光使用比较肤浅,研究一下原理和本质,更利于以后开发使用 扫盲贴 ...
- Python里的黄金库,学会了你的工资至少翻一倍
作者:[已重置]链接:https://zhuanlan.zhihu.com/p/26054228来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 阅读本文大概需要5分钟 ...
- 基于node的前端项目编译时内存溢出问题
解决方法: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory JavaScript堆内存不足,这里说的 Jav ...
- PyCharm远程连接服务器简明教程
转自本人知乎(https://zhuanlan.zhihu.com/p/149040742) 由于实验室的GPU都是放在远程服务器上,因此一直使用MobaXterm利用SSH远程跑实验,但是MobaX ...
- vue-admin-template搭建后台管理系统的学习(一)
首先我们来看看这个基础模版的目录结构 ├── build // 构建相关 ├── config // 配置相关├── src // 源代码│ ├── api // 所有请求│ ├── ass ...
- git常用代码合集
git常用代码合集 1. Git init:初始化一个仓库 2. Git add 文件名称:添加文件到Git暂存区 3. Git commit -m “message”:将Git暂存区的代码提交到Gi ...
- 必知必会的8个Python列表技巧
原作者:Nik Piepenbreier 翻译&内容补充:费弗里 原文地址:https://towardsdatascience.com/advanced-python-list-techni ...
- 利用c++中的设计灵感,既要学BIM分类信息表,借助GIS完成环境搭建改善
我,一个平平无奇的城市规划专业(建筑专业.路桥专业)大学生,还有一年要毕业,很担心工作以后受到社会的毒打,遂问导师和学长,我要自学点什么技能和软件? 学长A:CAD,SketchUp,PS我都很熟练了 ...
- Elasticsearch修改分词器以及自定义分词器
Elasticsearch修改分词器以及自定义分词器 参考博客:https://blog.csdn.net/shuimofengyang/article/details/88973597