最近一段时间遇到了几个小问题:
1、对接海康设备的一个回调函数里面调用一个service为空
2、引入其他服务时类找不到
其实这两个小问题排查起来也非常简单,归根结底就是bean没有注入到容器,或者说取不到容器里面的bean,这些年一直用springboot也没有仔细研究过bean容器相关知识,现在仔细学习一下
Springboot中Bean的生命周期
Bean生命周期一般有下面的四个阶段:
-
Bean的定义(Definitition):资源定位,根据注解(@Component)找到相应的类,解析资源并保存定义,Spring IoC容器装载Bean定义
-
Bean的实例化(Instantiation):给Bean对象分配内存空间
-
Bean的初始化(Initialization):属性赋值(依赖注入、对引用容器级别对象的属性赋值)
-
Bean的销毁(Destruction):即销毁
5. 网上找了张图详细描述了bean的生命周期,现在我主要尝试研究其他几个点来了解bean容器的相关关系和知识,这篇文章主要是围绕为什么bean注入不到容器以及容器为什么获取不到bean,所以其他部分不做探讨研究
图解信息:
-
创建beanfactory(扩展一)
-
① beanfactory要去定位资源,将资源封装成BeanDefinition(也就是bean 扩展二),这里出现第一个问题,如果没有定位到的资源是不是就不会注入到容器了,也就拿不到了,这里看结论一
-
解析完的BeanDefinition存入BeanFactory
-
② BeanFactory有后置处理器BeanFactoryPostProcessor,这里出现第二个问题,后置处理器这个东西一般都是可以获取对应的对象的,我们是不是可以在BeanFactory的后置处理器中获取到BeanFactory,从而主动向BeanFactory里面写入我们需要的bean,这里看结论二
-
③ BeanFactory通过反射实例化beanDefinition,这里出行第三个问题,是所有的beanDefinition都会被示例化吗?如果没有被实例化,自然也就取不到这个bean了,这里看结论三
-
初始化过程中有看到一个问题,具体看结论四
结论一:
资源定位不到自然注入不到容器里面,那么资源定位的逻辑是怎样的?我了解到有两个标准:
1. 扫描路径(basePackages)下所有类(默认是识别启动类所在包路径下的所有类 )
2. 扫码注解,@Component或@Import注解
-
关于1:这个原因是因为在启动类的@SpringBootApplication注解里面有@EnableAutoConfiguration->@AutoConfigurationPackage->@Import({Registrar.class}),通过@Import(@Import在扩展四有说明)将Registrar引入IOC容器里面,我们进入Registrar类可以看到方法:
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
AutoConfigurationPackages.register方法:
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
AutoConfigurationPackages.BasePackagesBeanDefinition beanDefinition = (AutoConfigurationPackages.BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);
beanDefinition.addBasePackages(packageNames);
} else {
registry.registerBeanDefinition(BEAN, new AutoConfigurationPackages.BasePackagesBeanDefinition(packageNames));
}
}
我们可以打断点发现packageNames其实就是启动类所在包的路径,所以就是在这里配置资源定位的默认路径的
-
关于2:Bean工厂后置处理器 - ConfigurationClassPostProcessor,该处理器在创建applicationContext时被注册,在刷新容器(refresh)方法中的invokeBeanFactoryPostProcessors方法中实例化,实例化后就会执行invokeBeanDefinitionRegistryPostProcessors,该方法会通过刚刚实例化出来的处理器来调用其扫描bean定义的方法,接着解析配置类(解析 @PropertySource, @ComponentScan,@Import, @ImportResource,@Bean 等注解),重点看一下解析@ComponentScan,里面会根据includeFilters的配置来扫描注解类,而Component注解就在其配置中,因此加了@Componet的注解的类会被扫码进。
-
由此可以得到结论1:要在默认路径下且加了@Component注解,或者通过@ComponentScan注解改变扫描的路径
参考:
Bean工厂后置处理器之ConfigurationClassPostProcessor
结论二:
既然有BeanFactory的后置处理器,那么就可以通过这个处理器获取到实例化之前的BeanFactory,我们是不是就可以自己把需要注入容器的bean通过BeanFactory注入了:
public class test implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 根据bean的名称获取bean的定义
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("beanDefinitionName");
// 获取bean的名称迭代器
Iterator<String> var1 = beanFactory.getBeanNamesIterator();
// beanFactory注册单例(添加bean)
beanFactory.registerSingleton("beanName", Object);
}
}
BeanFactoryPostProcessor是在bean的生命周期的实例化阶段前的refresh方法里面使用(AbstractApplicationContext.refresh())
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
// 这里是获取beanFactory的方法
// 可以看到beanFactory最终实现的类型是DefaultListableBeanFactory,观察这个类的属性有beanDefinitionMap,这个map就是存储bean的定义的,它的类型是ConcurrentHashMap,线程安全的map
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
...
...
...
同时,如结论一中提到的ConfigurationClassPostProcessor也是在这里执行的,会扫描很多有特点注解的类转成BeanDefinition
结论三:
看看实例化的代码:springboot源码路径:
run-》refreshContext-》refresh-》finishBeanFactoryInitialization-》preInstantiateSingletons
public void preInstantiateSingletons() throws BeansException {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Pre-instantiating singletons in " + this);
}
// 在这边打断点,可以看到已经有很多bean了,这些是springboot配置里面的bean,启动时自动注入,具体在
// spring-boot-2.7.5.jar!\META-INF\spring.factories里面
List<String> beanNames = new ArrayList(this.beanDefinitionNames);
Iterator var2 = beanNames.iterator();
while(true) {
String beanName;
Object bean;
do {
while(true) {
RootBeanDefinition bd;
do {
do {
do {
if (!var2.hasNext()) {
var2 = beanNames.iterator();
while(var2.hasNext()) {
beanName = (String)var2.next();
Object singletonInstance = this.getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize").tag("beanName", beanName);
SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton)singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(() -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, this.getAccessControlContext());
} else {
smartSingleton.afterSingletonsInstantiated();
}
smartInitialize.end();
}
}
return;
}
beanName = (String)var2.next();
bd = this.getMergedLocalBeanDefinition(beanName);
} while(bd.isAbstract());// 抽象
} while(!bd.isSingleton());// 不是单例
} while(bd.isLazyInit());// 懒加载
if (this.isFactoryBean(beanName)) {
bean = this.getBean("&" + beanName);
break;
}
this.getBean(beanName);
}
} while(!(bean instanceof FactoryBean));
FactoryBean<?> factory = (FactoryBean)bean;
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
SmartFactoryBean var10000 = (SmartFactoryBean)factory;
((SmartFactoryBean)factory).getClass();
isEagerInit = (Boolean)AccessController.doPrivileged(var10000::isEagerInit, this.getAccessControlContext());
} else {
isEagerInit = factory instanceof SmartFactoryBean && ((SmartFactoryBean)factory).isEagerInit();
}
if (isEagerInit) {
this.getBean(beanName);
}
}
}
三重do while结构,要跳出这个循环的bean才会进入到接下来的实例化和初始化中,所以要满足三个条件:非抽象类、非懒加载、是单例的对象,因此可以得出结论要想注入的bean必须是非抽象类、非懒加载的单例对象
可以参考:IOC流程解析-实例化和初始化 | 茯楚博客 但这个是spring,当前分析的是springboot,可以借鉴
结论四:
@Autowrite标记的类才能自动注入到类里面,而@Autowrite是通过AutowiredAnnotationBeanPostProcessor这个bean的后置处理器生效的,而bean的后置处理器是在bean的生命周期的初始化阶段生效的,故如果要使用@Autowrite标记的类,得在bean的初始化之后调用。
所以如果在一个类的构造方法里面使用@Autowrite标记的类,会报空指针,因为bean的初始化是在bean的生命周期的实例化阶段进行的,实例化阶段是在初始化阶段前面,此时@Autowrite注解不生效,故报错。(类的初始化是在bean的生命周期的实例化阶段,而bean的生命周期的初始化阶段是给bean注入属性的)
扩展一 - ApplicationContext说明
SpringApplication的run方法会先创建一个ApplicationContext,context继承了beans 的特性,为spring 核心提供了大量扩展,添加了对国际化(例如资源绑定)、事件传播、资源加载和对context 的透明创建的支持,最主要的功能就是利用beanFactory 创建bean 对象。
既然context是管理beanFactory的,那么在项目启动之后自然也可以通过context来获取容器里面的bean,这就是我们常用的方法context.getBean(BeanName),这个方法是通过实现aware接口实现的:
public class test implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
public Object getBean(String beanName) {
return this.context.getBean(beanName);
}
}
Aware接口是一个具有标识作用的超级接口,具体实现是有子接口去决定的,ApplicationContextAware接口可以通过重写setApplicationContext方法获取applicationContext,从而进行操作
扩展二 - BeanDefinition说明
beanDefinition 就是我们所有说的spring 的bean,我们自己定义的各个bean 其实会转换成一个beanDefinition 存在于spring 的beanFactory 中,beanDefinition 中保存了我们的bean 信息,比如这个bean 指向的是哪个类、是否是单例的、是否懒加载、这个bean 依赖了哪些bean 等等。java的普通的class不足以满足bean在生命周期中的操作,所以得用有丰富属性和方法的BeanDefinition来描述Bean。
bean的创建:什么是 BeanDefinition? 以及 Spring 怎么创建一个 bean_Cison chen的博客-CSDN博客
扩展三 - new出来的对象
new出来的对象里面使用bean会报错,因为new出来的对象未交予IOC容器管理。
扩展四 - @Import注解说明
@Import也是跟结论二提到的处理ComponentScan的地方一样的地方被处理,进入处理类processImports:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
上面有三个 if else 判断,分别处理三种场景:
- 通过 @Import 注解引入普通的 Java 类数组
- 通过 @Import 注解引入实现了 ImportSelector 接口的类
- 通过 @Import 引入实现了 ImportBeanDefinitionRegistrar 接口的类
通过此三种方法可以将类注入到容器里
扩展五 - @Import和@Component的区别
这个我暂时没找到相关参考资料,推测一下:
1. @Import是用数组形式注入bean,可以一下注入多个,而@Component是加在一个一个类上面的
2.@Import引入的是注解里面的参数,所以它可以写在其他注解上,而@Component作用的是一个类,所以如果你想定义某个注解,让他具有某种功能,可以用@Import导入相关类,这能形成具有复杂功能的注解或者类,解耦了?
3.ImportBeanDefinitionRegistrar 的形式还能对bean进行操作
扩展六 - Bean的实例化和初始化过程
-
实例化Bean 对象,这里的对象信息来源就是BeanFactory 中的BeanDefinition 对象,注意这里就只是单纯的实例化,没有参数赋值的那种
-
-
实例化:第 1 步,实例化一个 bean 对象;
-
属性赋值:第 2 步,为 bean 设置相关属性和依赖;
-
初始化:第 3~7 步,步骤较多,其中第 5、6 步为初始化操作,第 3、4 步为在初始化前执行,第 7 步在初始化后执行,该阶段结束,才能被用户使用;
-
销毁:第 8~10步,第8步不是真正意义上的销毁(还没使用呢),而是先在使用前注册了销毁的相关调用接口,为了后面第9、10步真正销毁 bean 时再执行相应的方法。
参考资料:
IOC流程源码详解(spring版):Spring的基本概念和IOC流程的简述 | 茯楚博客
生命周期:Spring中Bean的生命周期_Morning sunshine的博客-CSDN博客_spring中bean的生命周期
springboot自动装配源码:SpringBoot自动装配源码 - pluto_charon - 博客园
springboot源码:SpringBoot源码剖析 - 知乎