SpringBoot Bean容器相关问题研究学习(IOC)

最近一段时间遇到了几个小问题:

1、对接海康设备的一个回调函数里面调用一个service为空

2、引入其他服务时类找不到

其实这两个小问题排查起来也非常简单,归根结底就是bean没有注入到容器,或者说取不到容器里面的bean,这些年一直用springboot也没有仔细研究过bean容器相关知识,现在仔细学习一下

Springboot中Bean的生命周期

Bean生命周期一般有下面的四个阶段:

  1. Bean的定义(Definitition):资源定位,根据注解(@Component)找到相应的类,解析资源并保存定义,Spring IoC容器装载Bean定义

  2. Bean的实例化(Instantiation):给Bean对象分配内存空间

  3. Bean的初始化(Initialization):属性赋值(依赖注入、对引用容器级别对象的属性赋值)

  4. Bean的销毁(Destruction):即销毁

     5. 网上找了张图详细描述了bean的生命周期,现在我主要尝试研究其他几个点来了解bean容器的相关关系和知识,这篇文章主要是围绕为什么bean注入不到容器以及容器为什么获取不到bean,所以其他部分不做探讨研究

图解信息:

  1. 创建beanfactory(扩展一

  2.  beanfactory要去定位资源,将资源封装成BeanDefinition(也就是bean 扩展二),这里出现第一个问题,如果没有定位到的资源是不是就不会注入到容器了,也就拿不到了,这里看结论一

  3. 解析完的BeanDefinition存入BeanFactory

  4. BeanFactory有后置处理器BeanFactoryPostProcessor,这里出现第二个问题,后置处理器这个东西一般都是可以获取对应的对象的,我们是不是可以在BeanFactory的后置处理器中获取到BeanFactory,从而主动向BeanFactory里面写入我们需要的bean,这里看结论二

  5. BeanFactory通过反射实例化beanDefinition,这里出行第三个问题,是所有的beanDefinition都会被示例化吗?如果没有被实例化,自然也就取不到这个bean了,这里看结论三

  6. 初始化过程中有看到一个问题,具体看结论四

结论一:

        资源定位不到自然注入不到容器里面,那么资源定位的逻辑是怎样的?我了解到有两个标准:

        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注解改变扫描的路径

参考:

  @Component源码解析逻辑 

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 注解使用及原理解析

扩展五 - @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源码剖析 - 知乎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值