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

目录
  • @Autowired依赖注入问题–逻辑使用先于@Autowired注解处理
    • 测试用例
  • BeanFactory.getBean问题–getBean调用先于BeanDefinition信息注册
  • 在Configuration中使用@Autowired注解
    • spring 实例化Bean过程
    • @Bean内部使用配置类@Autowired注解引入依赖
  • InitializingBean#afterPropertiesSet内部使用依赖
  • 总结

最近有空总结一下之前在使用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依赖注入问题–逻辑使用先于@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问题–getBean调用先于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 对@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的依赖时,到底在哪个时机,就能分析清楚是不是适合使用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 详解SpringBoot中实现依赖注入功能

    今天给大家介绍一下SpringBoot中是如何实现依赖注入的功能. 在以往spring使用中,依赖注入一般都是通过在Spring的配置文件中添加bean方法实现的,相对于这个方式SpringBoot的实现方式就显得非常便捷了.SpringBoot的实现方式基本都是通过注解实现的. 下面来看一下具体案例,这里我编写了三个测试类用于测试依赖注入到底是否可以正确实现. TestBiz接口: package example.biz; public interface TestBiz { public S

  • 基于SpringBoot构造器注入循环依赖及解决方式

    1. 循环依赖是什么? Bean A 依赖 B,Bean B 依赖 A这种情况下出现循环依赖. Bean A → Bean B → Bean A 更复杂的间接依赖造成的循环依赖如下. Bean A → Bean B → Bean C → Bean D → Bean E → Bean A 2. 循环依赖会产生什么结果? 当Spring正在加载所有Bean时,Spring尝试以能正常创建Bean的顺序去创建Bean. 例如,有如下依赖: Bean A → Bean B → Bean C Spring

  • 详析Spring中依赖注入的三种方式

    前言 平常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中.依赖注入的另一种说法是"控制反转",通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做. 在Sprin

  • 关于Spring的@Autowired依赖注入常见错误的总结

    做不到雨露均沾 经常会遇到,required a single bean, but 2 were found. 根据ID移除学生 DataService是个接口,其实现依赖Oracle: 现在期望把部分非核心业务从Oracle迁移到Cassandra,自然会先添加上一个新的DataService实现: @Repository @Slf4j public class CassandraDataService implements DataService{ @Override public void

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

    目录 @Autowired依赖注入问题–逻辑使用先于@Autowired注解处理 测试用例 BeanFactory.getBean问题–getBean调用先于BeanDefinition信息注册 在Configuration中使用@Autowired注解 spring 实例化Bean过程 @Bean内部使用配置类@Autowired注解引入依赖 InitializingBean#afterPropertiesSet内部使用依赖 总结 最近有空总结一下之前在使用spring boot时遇到过的几种

  • Spring框架中IoC容器与DI依赖注入教程

    目录 一.Spring 是什么 1.1 什么是容器 1.2 什么是 IoC 二.理解 IoC 2.1 传统程序开发的问题 2.2 分析 2.3 控制反转式程序开发 2.4 对比总结规律 2.5 理解 Spring IoC 三.DI 概念说明 四.总结 一.Spring 是什么 我们通常所说的 Spring 指的是 Spring Framework (Spring 框架),它是⼀个开源框架,有着活跃而庞大的社区,这就是它之所以能长久不衰的原因.Spring ⽀持⼴泛的应⽤场景,它可以让 Java

  • 关于spring boot中几种注入方法的一些个人看法

    前言 最近在知乎上面看到一篇关于程序员面试的问题,面试官问我们一般有几种注入的方法,这几种注入的方法分别在什么时候运用比合理,当时我看到这个时候懵逼了,由于我自己也是刚刚接触springboot不久,所以就自己在平时运用的上面总结了一些知识点常用的几种springboot的注入方法,由于我是一个小萌新,所只要是能够起道注入的方法的注解我都列出来,有可能会有错,希望大家能够及时提出来我来解决: @Autowired @Resource @Component @Configuration @Qual

  • 实例讲解Java的Spring框架中的控制反转和依赖注入

    近来总是接触到 IoC(Inversion of Control,控制反转).DI(Dependency Injection,依赖注入)等编程原则或者模式,而这些是著名 Java 框架 Spring.Struts 等的核心所在.针对此查了 Wikipedia 中各个条目,并从图书馆借来相关书籍,阅读后有些理解,现结合书中的讲解以及自己的加工整理如下: eg1 问题描述: 开发一个能够按照不同要求生成Excel或 PDF 格式的报表的系统,例如日报表.月报表等等.   解决方案: 根据"面向接口编

  • 详解Spring Boot 中实现定时任务的两种方式

    在 Spring + SpringMVC 环境中,一般来说,要实现定时任务,我们有两中方案,一种是使用 Spring 自带的定时任务处理器 @Scheduled 注解,另一种就是使用第三方框架 Quartz ,Spring Boot 源自 Spring+SpringMVC ,因此天然具备这两个 Spring 中的定时任务实现策略,当然也支持 Quartz,本文我们就来看下 Spring Boot 中两种定时任务的实现方式. @Scheduled 使用 @Scheduled 非常容易,直接创建一个

  • Spring Boot 中密码加密的两种方法

    先说一句:密码是无法解密的.大家也不要再问松哥微人事项目中的密码怎么解密了! 密码无法解密,还是为了确保系统安全.今天松哥就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全. 1.为什么要加密 2011 年 12 月 21 日,有人在网络上公开了一个包含 600 万个 CSDN 用户资料的数据库,数据全部为明文储存,包含用户名.密码以及注册邮箱.事件发生后 CSDN 在微博.官方网站等渠道发出了声明,解释说此数据库系 2009 年备份所用,因不明原因泄露,已经向警方报案,后又在

  • 详解Spring Boot中初始化资源的几种方式

    假设有这么一个需求,要求在项目启动过程中,完成线程池的初始化,加密证书加载等功能,你会怎么做?如果没想好答案,请接着往下看.今天介绍几种在Spring Boot中进行资源初始化的方式,帮助大家解决和回答这个问题. CommandLineRunner 定义初始化类 MyCommandLineRunner 实现 CommandLineRunner 接口,并实现它的 run() 方法,在该方法中编写初始化逻辑 注册成Bean,添加 @Component注解即可 示例代码如下: @Component p

  • Spring Boot中@RequestParam参数的5种情况说明

    目录 Spring Boot中@RequestParam参数的5种情况 实例如下: Spring Boot注解:@RequestParam详解 1.value:参数名字,即入参的请求参数名字 ​2.required:该参数是否为必传项. ​3.defaultValue:参数的默认值 Spring Boot中@RequestParam参数的5种情况 实例如下: // 可带参数可不带参数,方法都能执行 @RequestMapping("/list") public String test1

  • Spring Boot中使用Spring-data-jpa实现数据库增删查改

    在实际开发过程中,对数据库的操作无非就"增删改查".就最为普遍的单表操作而言,除了表和字段不同外,语句都是类似的,开发人员需要写大量类似而枯燥的语句来完成业务逻辑. 为了解决这些大量枯燥的数据操作语句,我们第一个想到的是使用ORM框架,比如:Hibernate.通过整合Hibernate之后,我们以操作Java实体的方式最终将数据改变映射到数据库表中. 为了解决抽象各个Java实体基本的"增删改查"操作,我们通常会以泛型的方式封装一个模板Dao来进行抽象简化,但是这

  • Spring Boot中使用 Spring Security 构建权限系统的示例代码

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作. 权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重.在Spring Boot中使用 Spring Security 构建权限系统是非常轻松和简单的.下面我们就来快速入门 Spring Security .在开始前我们需要一对

随机推荐