服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 详谈spring boot中几种常见的依赖注入问题

详谈spring boot中几种常见的依赖注入问题

2022-01-21 01:00一撸向北 Java教程

这篇文章主要介绍了spring boot中几种常见的依赖注入问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

最近有空总结一下之前在使用spring boot时遇到过的几种依赖注入时的坑,如果不了解spring内部的处理过程,使用起来总是感觉有种迷糊。

在分析场景前,需要大概了解一下spring对于bean的实例化过程是需要先注册BeanDefinition信息然后才进行实例化,在org.springframework.context.support.AbstractApplicationContext#refresh中定义的基本的流程。部分代码

		try {
			// Allows post-processing of the bean factory in context subclasses.
			postProcessBeanFactory(beanFactory);
			// 1. 包含了BeanDefinition注册过程
			invokeBeanFactoryPostProcessors(beanFactory);
			// Register bean processors that intercept bean creation.
			registerBeanPostProcessors(beanFactory);
			// Initialize message source for this context.
			initMessageSource();
			// Initialize event multicaster for this context.
			initApplicationEventMulticaster();
			// Initialize other special beans in specific context subclasses.
			onRefresh();
			// Check for listener beans and register them.
			registerListeners();
			// 2. 根据BeanDefinition处理Bean实例化过程
			finishBeanFactoryInitialization(beanFactory);
			// Last step: publish corresponding event.
			finishRefresh();
		}

 

@Autowired依赖注入问题C逻辑使用先于@Autowired注解处理

之前不熟悉spring bean的实例化过程可能会遇到的坑就是使用@Autowired依赖注入的对象是null没有注入到相应的对象里面,或者准确的来说是在我程序的某一块逻辑代码执行时使用到@Autowired依赖的bean,但是bean确实null。

这种场景一般就是在当时,@Autowired注解并没有被处理,所以依赖的bean为null。

如果了解spring boot自动化装配可以直达我们通过实现ImportBeanDefinitionRegistrar接口自定义注册BeanDefinition信息,实现逻辑是ImportBeanDefinitionRegistrar#registerBeanDefinitions的具体实现中,这个方法的执行过程属于

​// 1. 包含了BeanDefinition注册过程
invokeBeanFactoryPostProcessors(beanFactory);

之前曾经遇到过在自定义的ImportBeanDefinitionRegistrar实现类中,使用@Autowired依赖某个bean,但是在使用时无法得到具体的实现对象,因为@Autowired注解的处理过程是在

​//2. 根据BeanDefinition处理Bean实例化过程
​finihBeanFactoryInitialization(beanFactory);

当程序执行ImportBeanDefinitionRegistrar#registerBeanDefinitions时,依赖的bean为null,报空指针。

测试用例

引导程序代码

@SpringBootApplication(scanBasePackages = "garine.learn.test.auwired")
@Import(TestAutowiredRegistar.class)
public class BootstrapTestApplication3 {
  public static void main(String[] args) {
      ApplicationContext applicationContext = SpringApplication.run(BootstrapTestApplication3.class, args);
  }
}
@Component
public class TestAutowiredRegistar implements ImportBeanDefinitionRegistrar,BeanFactoryAware,InitializingBean{
  private BeanFactory beanFactory;
  @Autowired
  TestRegistarDependOn testRegistarDependOn;
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +testRegistarDependOn);
  }
  @Override
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
      this.beanFactory = beanFactory;
  }
  @Override
  public void afterPropertiesSet() throws Exception {
      System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" + testRegistarDependOn);
  }
}
@Component
public class TestRegistarDependOn {
}

执行输出:TestAutowiredRegistar-----null

**这种场景一般就是在当时,@Autowired注解并没有被处理,所以依赖的bean为null。**如果遇到依赖注入为空时,如果确定已经定义了对应的bean,那么不妨看看代码使用依赖bean时,到底@Autowired注解有没有被处理。

这种场景的解决办法就是使用BeanFactory来获取bean,修改代码如下。

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +testRegistarDependOn);
  System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +beanFactory.getBean(TestRegistarDependOn.class));
}

再次执行输出:

TestAutowiredRegistar-----null

TestAutowiredRegistar-----garine.learn.test.auwired.TestRegistarDependOn@7808fb9

 

BeanFactory.getBean问题CgetBean调用先于BeanDefinition信息注册

这里就是beanFactory.getBean方法如果获取不到bean就会调用bean的初始化过程。但是需要注意bean对应的BeanDefinition信息必须已经注册完成。所以这种getBean的方式不是绝对安全。

一般而言ConfigurationClassParser#processConfigurationClass为入口,可以看到整个对Configclass的处理过程。对于@Configuration标注的类都是有排序的,排序在前的先进行处理。

那么会不会出现在ImportBeanDefinitionRegistrar#registerBeanDefinitions中使用beanFactory.getBean方法获取bean而报错的场景呢?答案是会,假如定义两个@Configuration标注的类,a和b,a先于b处理,a通过@Import导入TestAutowiredRegistar,b中定义TestRegistarDependOn的bean实例化方法,代码如下。

配置类:

@Configuration
@Order(1)
@Import(TestAutowiredRegistar.class)
public class FirstConfig {
  //1.先处理TestAutowiredRegistar的ImportBeanDefinitionRegistrar#registerBeanDefinitions
@Configuration
@Order(2)
public class SecondConfig {
  @Bean
  public TestRegistarDependOn testRegistarDependOn(){
      return new TestRegistarDependOn();
  }
}

TestRegistarDependOn去掉@Component注解,避免被扫描到提前注册BeanDefinition

引导程序,去掉提前Import TestAutowiredRegistar.class

@SpringBootApplication(scanBasePackages = "garine.learn.test.auwired")
public class BootstrapTestApplication3 {
  public static void main(String[] args) {
      ApplicationContext applicationContext = SpringApplication.run(BootstrapTestApplication3.class, args);
  }
}

执行启动程序,输出报错信息

A component required a bean of type ‘garine.learn.test.auwired.TestRegistarDependOn' that could not be found.

所以实际上在getBean时,如果bean的BeanDefinition并没有注册到Beanfactory,那么久会报出上述错误。

把两个配置类的@Order顺序换一下,就能处理成功,执行输出。--------------------------然并卵。。。一样报错,

what?源码里面明明有排序的,在

org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

对所有的配置类都有

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
 int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
 int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
 return Integer.compare(i1, i2);
});

经过调试发现,应该是在spring.factories中定义的Configuration类才会在这里做处理,可以称之为最高优先级配置,对于这些配置@Order才会起作用。

那么我们自定义的@Configuration标注的类在哪里处理?经过调试,定位在@ComponentScan注解处理处,有如下代码。

// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
    sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
    !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
 for (AnnotationAttributes componentScan : componentScans) {
    // The config class is annotated with @ComponentScan -> perform the scan immediately
    Set<BeanDefinitionHolder> scannedBeanDefinitions =
          this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
    // Check the set of scanned definitions for any further config classes and parse recursively if needed
    for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
       BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
       if (bdCand == null) {
          bdCand = holder.getBeanDefinition();
       }
       //判断扫描到的是不是配置类,是的话就进行配置处理
       if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
          parse(bdCand.getBeanClassName(), holder.getBeanName());
       }
    }
 }
}

也就是说,我们一般自定义的配置顺序@Order是不起作用的,全靠扫描文件得到的先后顺序,所以,文件名称是关键。。

这里把FirstConfig改成TFirstConfig试试,输出

TestAutowiredRegistar-----null

TestAutowiredRegistar-----garine.learn.test.auwired.TestRegistarDependOn@189b5fb1

所以猜想通过。总结就是@Order对spring.factories中定义的配置类起作用,我们自定义的配置类处理顺序需要文件名称来控制。

 

在Configuration中使用@Autowired注解

通过调试都可以知道@Bean注册实例的方式,实际代理调用@Bean修饰的方法是在

// 2. 根据BeanDefinition处理Bean实例化过程
finishBeanFactoryInitialization(beanFactory);

的过程中的,所以假如在@Bean修饰的方法中使用到@Autowired注解依赖的bean是怎么样的场景?

spring 实例化Bean过程

先了解一下实例化bean的过程是怎么样的。在finishBeanFactoryInitialization中,遍历所有的BeanDefinition信息来实例化bean。遍历的顺序调试几次后发现是按照注册BeanDefinition信息的先后顺序。所以可以有几个简单规则可以判断哪个bean会先实例化。

  • 同是@ComponentScan扫描注册的bean,按照class文件名顺序,排在前面的先注册,所以先实例化,例如 ATest类实例化先于BTest类.
  • @Conguration 配置类实例化先于其内部定义的@Bean方法执行实例化,例如Config类实例化先于其内部任意@Bean 方法实例化bean。

那么考虑,假如在@Conguration 修饰的类的@Bean方法里面使用@Autowired引入依赖,而这个依赖实例化顺序要比@Conguration 修饰的类要迟,会怎么样?

定义下面三个类,在同一个包里面顺序也是如下所示:

  • ConfigInitBean
  • TestDependOnConfig
  • ZTestConfigurationDependOn

各自代码如下:

public class ConfigInitBean {
}
@Configuration
public class TestDependOnConfig {
  @Autowired
  ZTestConfigurationDependOn zTestConfigurationDependOn;//观察这个依赖什么时候进行初始化,断点getBean调试
  @Bean
  public ConfigInitBean configInitBean(){
      zTestConfigurationDependOn.saySome();
      return new ConfigInitBean();
  }
}
@Component
public class ZTestConfigurationDependOn {
  public void saySome(){
      System.out.println("say some");
  }
}

在DefaultListableBeanFactory#preInstantiateSingletons方法中断点查看beanNames的顺序

详谈spring boot中几种常见的依赖注入问题

根据spring 对@Configuration标注的类的处理过程,能够对应的上,先扫描到TestDependOnConfig所以先注册,ZTestConfigurationDependOn后扫描所以比TestDependOnConfig实例化要晚。ConfigInitBean是由@Bean定义的,在对配置类的处理中,都是先处理完@ComponentScan的BeanDefinition注册,再处理@Bean、@Import导入的配置、@ImportResource导入的xml等等BeanDefinition注册。

具体可以看有关自动装配的文章

总结来说就是bean实例化的顺序符合猜想,实际上还有一点就是每个bean实例化时,都会对其@Autowired注解的依赖进行注入,如果当时依赖没有实例化,就根据依赖的BeanDefinition进行getBean过程所以一般情况下,我们平常使用业务代码模型都不会出现注入为null问题。

当然,如果依赖的Beandefinition不存在,那么就会报错:

Consider defining a bean of type ‘XXXx' in your configuration.

在这里例子中,TestDependOnConfig依赖ZTestConfigurationDependOn,但是比ZTestConfigurationDependOn实例化要早,所以会调用getBean.ZTestConfigurationDependOn,提前实例化ZTestConfigurationDependOn来注入依赖。

具体源码在AbstractAutowireCapableBeanFactory#populateBean方法中,填充bean。

for (BeanPostProcessor bp : getBeanPostProcessors()) {
 if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    //填充属性
    pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    if (pvs == null) {
       return;
    }
 }
}

填充具体使用的实现方法是AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues来进行填充属性,最终会调用依赖bean的getBean,从而实例化依赖的bean或者直接获取依赖bean。

所以就算是配置类也好,普通的组件也好,都会在实例化时注入@Autowired依赖的属性实例,如果是该实例没有定义BeanDefinition,那么就会无法注入。

@Bean内部使用配置类@Autowired注解引入依赖

了解完上面的过程,可以知道,@Bean方法是在finishBeanFactoryInitialization过程实例化对应的bean时才会被代理调用,并且顺序比对应配置类要后,这时对应配置类早已经实例化完毕,依赖属性也已经注入,可以放心在@Bean方法内部使用。

还有一种情况是,假如@Bean方法被提前调用,例如@Bean的实例被另一个比@Bean所在配置类还要早实例化的组件中引入,那么此时@Bean所在配置类还没实例化,这样调用会出错吗?答案是不会,因为归根到底,@Bean方法的调用都是代理方式,程序还是需要先实例化一个@Bean所在配置类的实例,才能进行@Bean方法的调用,从而实例化一个@Bean方法的bean。

 

InitializingBean#afterPropertiesSet内部使用依赖

了解到上面的知识,推测一下,在InitializingBean#afterPropertiesSet里面使用@Autowired依赖进行逻辑处理是否可以?看如下InitializingBean#afterPropertiesSet的调用时机。

	try {
	//填充依赖属性
		populateBean(beanName, mbd, instanceWrapper);
		//最终调用到InitializingBean#afterPropertiesSet方法
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}

所以,InitializingBean#afterPropertiesSet是在填充玩依赖属性之后调用的,因此可以使用依赖的bean进行一些逻辑操作的。

 

总结

1、所以总结来说就是,我们的代码逻辑在

// 根据BeanDefinition处理Bean实例化过程
finishBeanFactoryInitialization(beanFactory);

过程之前都没有使用的@Autowired依赖bean的话,那是没问题的,因为@Autowired注解处理都是在finishBeanFactoryInitialization()也就是bean实例化时才会进行处理。如果使用了,那就是空指针;

2、在@Bean方法内部使用@Autowired注解的依赖,只要设计好程序也是可以的的,只要依赖的BeanDefinition已经注册过,配置类实例化时就能主动发起依赖的实例化过程,然后注入依赖,不会出现空指针。

而@Bean方法是在finishBeanFactoryInitialization过程实例化对应的bean时才会被代理调用,并且顺序比对应配置类要后,这时对应配置类早已经实例化完毕,依赖属性也已经注入,可以放心在@Bean方法内部使用。

所以这里@Bean方法实例化bean时如果使用到@Autowired依赖的bean时,就对配置类的实例有很强的依赖性,这种依赖顺序spring都帮我们保证先实例化配置类,再调用@Bean方法。

3、InitializingBean#afterPropertiesSet内部使用也是没问题,原理如上。所以只要理解在使用到@Autowired的依赖时,到底在哪个时机,就能分析清楚是不是适合使用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/qq_20597727/article/details/82833843

延伸 · 阅读

精彩推荐
  • Java教程20个非常实用的Java程序代码片段

    20个非常实用的Java程序代码片段

    这篇文章主要为大家分享了20个非常实用的Java程序片段,对java开发项目有所帮助,感兴趣的小伙伴们可以参考一下 ...

    lijiao5352020-04-06
  • Java教程Java8中Stream使用的一个注意事项

    Java8中Stream使用的一个注意事项

    最近在工作中发现了对于集合操作转换的神器,java8新特性 stream,但在使用中遇到了一个非常重要的注意点,所以这篇文章主要给大家介绍了关于Java8中S...

    阿杜7482021-02-04
  • Java教程Java实现抢红包功能

    Java实现抢红包功能

    这篇文章主要为大家详细介绍了Java实现抢红包功能,采用多线程模拟多人同时抢红包,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙...

    littleschemer13532021-05-16
  • Java教程xml与Java对象的转换详解

    xml与Java对象的转换详解

    这篇文章主要介绍了xml与Java对象的转换详解的相关资料,需要的朋友可以参考下...

    Java教程网2942020-09-17
  • Java教程Java BufferWriter写文件写不进去或缺失数据的解决

    Java BufferWriter写文件写不进去或缺失数据的解决

    这篇文章主要介绍了Java BufferWriter写文件写不进去或缺失数据的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望...

    spcoder14552021-10-18
  • Java教程小米推送Java代码

    小米推送Java代码

    今天小编就为大家分享一篇关于小米推送Java代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧...

    富贵稳中求8032021-07-12
  • Java教程升级IDEA后Lombok不能使用的解决方法

    升级IDEA后Lombok不能使用的解决方法

    最近看到提示IDEA提示升级,寻思已经有好久没有升过级了。升级完毕重启之后,突然发现好多错误,本文就来介绍一下如何解决,感兴趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    这篇文章主要介绍了Java使用SAX解析xml的示例,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下...

    大行者10067412021-08-30