【面试题】Spring IoC与DI、自动装配与循环依赖

Spring IoC是Spring最经典设计,自动装配是IoC注入“自动化”的一个简化配置操作。尽管IoC注入帮我们管理了对象之间的依赖关系,但是仍可能发生设计不当而导致了循环依赖问题。强大Spring也提供了一些优雅的解决方案。

目录

什么是IoC

IoC与控制反转

如何理解IoC容器

IoC注入方式

优缺点比较

自动装配

装配方式

循环依赖

解决办法


什么是IoC

Spring IoC容器负责对象的生命周期和对象之间的(依赖)关系。
在创建新的Bean时,IoC容器会自动注入新Bean的所依赖的其他Bean,而无须自己手动创建。
这里写图片描述

  • IoC容器自动完成对象的初始化,避免在开发过程中写一大段初始化代码。
  • 创建实例的时候不需要了解细节。

优点:
不会对业务代码构成很强的侵入性,对象具有更好的可测试性,可重用性和可拓展性。

优点看起来很抽象,可以结合实例来理解:参见王福强的《Spring揭秘》2.3节IoC的附加值

IoC与控制反转

IoC 全称为 InversionofControl,翻译为 “控制反转”.

  1. 谁控制谁:在传统的开发模式下,我们都是采用直接 new 一个对象的方式来创建对象,也就是说你依赖的对象直接由你自己控制,但是有了 IOC 容器后,则直接由 IoC 容器来控制。所以“谁控制谁”,当然是 IoC 容器控制对象。
  2. 控制什么:控制对象。
  3. 为何是反转:没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转。
  4. 哪些方面反转了:所依赖对象的获取被反转了。

普遍认为(误,下面会讲),IoC还有一个别名为 DI( DependencyInjection),即依赖注入。“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

  1. 谁依赖谁:“被注入的对象”依赖“依赖对象”。举个例子,对象A依赖B,那么IoC容器在注入A对象之前,需要先注入B对象;对象A依赖IoC容器;对象B被注入到对象A中,所以A是被注入的对象,B是依赖对象,A依赖B。
  2. 为什么需要依赖:容器管理对象需要IoC容器来提供对象需要的外部资源;
  3. 谁注入谁:很明显是IoC容器注入某个对象,也就是注入“依赖对象”;
  4. 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

二者的关系
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。

如何理解IoC容器

我们自己手动创建一个车instance时候,是从底层往上层new的:
这里写图片描述
每一个类的构造函数都直接调用了底层代码的构造函数。这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。底层的改动都会牵动所有上层的改动。如果每次修改一个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历)。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。
这里写图片描述
这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:
这里写图片描述
我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。

IoC注入方式

  • 构造器注入
    被注入对象的构造方法的参数列表声明依赖对象的参数列表
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}

Spring4.3+之后,constructor注入支持非显示注入方式。

private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

// @Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}
  • setter方法注入
    被注入对象的setter()方法的参数列表声明依赖对象
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public void setDependencyA(DependencyA dependencyA) {
    this.dependencyA = dependencyA;
}

@Autowired
public void setDependencyB(DependencyB dependencyB) {
    this.dependencyB = dependencyB;
}

@Autowired
public void setDependencyC(DependencyC dependencyC) {
    this.dependencyC = dependencyC;
}
  • 接口注入
    比较繁琐。已经极少使用了。
    被注入对象实现的接口方法的参数列表声明依赖对象
  • 字段注入
@Autowired
private DependencyA dependencyA;

@Autowired
private DependencyB dependencyB;

@Autowired
private DependencyC dependencyC;

优缺点比较

注入方式 优点 缺点 实用场景
字段注入 简单,便于添加新的dependency 可能会出现注入失败而出现NullPointedException;在Test和其他Module不可用;不可用于final字段,从而无法保证字段的不变性。 Spring官方不推荐这种写法。
setter注入 灵活性高,便于修改依赖对象 对于仅使用setter注入的依赖对象需要进行非空检查;对象无法在构造完成后马上进入就绪状态 重新注入非默认值(构造器注入)的依赖对象;对于非必需的依赖,建议使用setter注入。
constructor注入 对象在构造完成之后,即已进入就绪状态,可以马上使用。 当依赖对象比较多的时候,构造方法的参数列表会比较长,维护和使用也比较麻烦,根据单一职责原则,此时应该考虑重构了。使用不慎还有可能出现循环依赖。  

Spring4.x之后,注入方式应该按需选择setter或constructor注入方式。

官方为什么不推荐字段注入

  1. 单一职责侵入
    添加依赖是很简单的,可能过于简单了。添加六个、十个甚至一堆依赖一点也没有困难,使得我们不易察觉违反单一职责原则的程序。
    而使用构造器注入方法时,发现违反单一职责原则的程序则相对容易了。在使用构造器方式注入时,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现something is wrong。拥有太多的依赖通常意味着你的类要承担更多的责任。明显违背了单一职责原则(SRP:Single responsibility principle)。
  2. 无法声明不变的字段。
    字段注人无法注入final字段,只有构造器注入才能注入final字段
  3. 隐藏了依赖关系
    使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责从类中抽离出来,IoC容器会帮你装配。当类不再为依赖对象负责,它应该更明确的使用公有的接口方法或构造器,使用这种方式能很清晰的了解类需要什么,也能明确它是可选的(setter注入)还是强制的(构造器注入)。
  4. 依赖注入容器紧耦合
    依赖注入框架的核心思想之一就是受容器管理的类不应该去依赖容器所使用的依赖对象。换句话说,这个类应该是一个简单的POJO(Plain Ordinary Java Object)能够被单独实例化并且你也能为它提供它所需的依赖。只有这样,你才能在单元测试中实例化这个类而不必去启动依赖注入容器,实现测试分离(启动容器更多是集成测试)。
    然而,当使用变量直接注入时,没有一种方式能直接地实例化这个类并且满足其所有的依赖。这意味着需要手动new出来依赖对象或者只能在IoC Container范围使用。

自动装配

自动装配是为了将依赖注入“自动化”的一个简化配置的操作。
当一个对象的属性是另一个对象时,实例化时,需要为这个对象属性进行实例化。这就是装配。
如果一个对象只通过接口来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行切换。但是这样会存在一个问题,在传统的依赖注入配置中,我们必须要明确要给属性装配哪一个bean的引用,一旦bean很多,就不好维护了。基于这样的场景,spring使用注解来进行自动装配,解决这个问题。自动装配就是开发人员不必知道具体要装配哪个bean的引用,这个识别的工作会由spring来完成。与自动装配配合的还有“自动检测”,这 个动作会自动识别哪些类需要被配置成bean,进而来进行装配。这样我们就明白了,自动装配是为了将依赖注入“自动化”的一个简化配置的操作。

装配方式

  • byName就是会将与属性的名字一样的bean进行装配。
  • byType就是将同属性一样类型的bean进行装配。-- constructor就是通过构造器来将类型与参数相同的bean进行装配。
  • autodetect是constructor与byType的组合,会先进行constructor,如果不成功,再进行byType。

具体选择哪一种装配方式,需要配置标签的autowire属性,如果没有配置,默认是byName类型,就是会根据属性的名字来进行自动装配。上面最常用的还是byName和byType。自动装配时,装配的bean必须是唯一与属性进行吻合的,不能多也不能少,有且只有一个可以进行装配的bean,才能自动装配成功。否则会抛出异常。如果要统一所有bean的自动装配类型,可以在标签中配置default-autowire属性。当然如果配置了autowire属性,我们依然可以手动装配属性,手动装配会覆盖自动装配。

spring2.5之后提供了注解方式的自动装配。但是要使用这些注解,需要在配置文件中配置<context:annotation-config />。只有加上这一配置,才可以使用注解进行自动装配,默认情况下基于注解的装配是被禁用的。
常用的自动装配注解有以下几种:@Autowired,@Qualifier(@Resource, @Inject, @Named为JavaEE的标准,不建议使用)。
@Autowired注解是byType类型的,这个注解可以用在属性上面,setter方面上面以及构造器上面。使用这个注解时,就不需要在类中为属性添加setter方法了。但是这个属性是强制性的,也就是说必须得装配上,如果没有找到合适的bean能够装配上,就会抛出异常:`NoSuchBeanDefinitionException,如果required=false时,则不会抛出异常。另一种情况是同时有多个bean是一个类型的,也会抛出这个异常。此时需要进一步明确要装配哪一个Bean,这时可以组合使用@Qualifier注解,值为Bean的名字即可。@Qualifier注解使用byName进行装配,这样可以在多个类型一样的bean中,明确使用哪一个名字的bean来进行装配。@Qualifier注解起到了缩小自动装配候选bean的范围的作用。

自动检测配置,也是springmvc中最牛的一项功能。只要一个配置<context:component-scan base-package="">或者注解@ComponentScan("")
,base-package属性指定要自动检测扫描的包。
该配置会自动扫描指定的包及其子包下面被构造型注解标注的类,并将这些类注册为spring bean,这样就不用在配置文件一个一个地配置成bean标签。构造型注解包括:@Controller,@Components,@Service,@Repository和使用@Component标注的自定义注解。生成的bean的ID默认为类的非限定名,也就是把类的名字的首字母换成小写。可以在这些注解的值中写名bean id的值,如@Controller(“helloworld”)。如果你想细化包被扫描的范围,可以使用<context:include-filter><context:exclude-filter>。具体使用方法这里不再详说。注意,没有被扫描到的类是不能注册为bean,也就不能被用来装配其他类。所以这个配置的base-package的范围非常重要。

循环依赖

依赖注入稍不注意就会出现循环依赖:
Bean之间的依赖顺序: BeanA -> BeanB -> BeanA
举个例子:

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

运行SpringBootApplication:

@SpringBootApplication
@ComponentScan("com.example.circulardependency.constructor")
public class CirculardependencyApplication {

    public static void main(String[] args) {
        SpringApplication.run(CirculardependencyApplication.class, args);
    }
}

会报以下的错:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?

还会画出dependency circle:
这里写图片描述

解决办法

出现循环依赖是因为设计问题,最佳处理方法是重新设计

https://zhuanlan.zhihu.com/p/84267654

在实际开发中,推倒重来往往是不允许的,所以会有以下几种补救方法。

1.改用setter注入方式(推荐)

与constructor注入不同,setter是按需注入的,并且允许依赖对象为null

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}

加单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
@ComponentScan("com.example.circulardependency.setter")
public class CirculardependencyApplicationTests {
    @Bean
    public CircularDependencyB getDependencyB() {
        return new CircularDependencyB();
    }

    @Bean
    public CircularDependencyA getDependencyA() {
        CircularDependencyA circularDependencyA = new CircularDependencyA();
        circularDependencyA.setCircularDependencyB(getDependencyB());
        return circularDependencyA;
    }

    @Test
    public void contextLoads() {
        System.out.println("Hello world.");
        CircularDependencyA circularDependencyA = getDependencyA();
        System.out.println(circularDependencyA.getCircularDependencyB().getMessage());
    }
}

使用字段注解也可以解决循环依赖,但是字段注解为非官方推荐做法,所以在这里也就不给出实例了。

2. @Lazy注解

@Lazy延迟初始化。在本例中,会先构建 CircularDependencyA完成后, 再构建CircularDependencyB,打破dependency circle。

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

3. 使用ApplicationContextAware, InitializingBean

ApplicationContextAware获取SpringContext,用于加载bean;InitializingBean定义了设置Bean的property之后的动作。

@Component
public class CircularDependencyA implements InitializingBean, ApplicationContextAware {
    private CircularDependencyB circB;
    private ApplicationContext context;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        this.circB = context.getBean(CircularDependencyB.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    public CircularDependencyB getCircularDependencyB() {
        return circB;
    }
}

原理:跟Spring Bean的Life cycle有关:先实例化,再调用setApplicationContext(), afterPropertiesSet(); 所以,CircularDependencyB已经加载完毕后,再加载CircularDependencyA。
倘若,CircularDependencyA先实例化, 调用afterPropertiesSet()时发现CircularDependencyB尚未加载,先加载CircularDependencyB(调用构造函数,由于此时CircularDependencyA已经实例化了,所以能够顺利加载),设置属性circB,CircularDependencyA加载完成。

原文:https://blog.csdn.net/programmer_at/article/details/82389221
[1]IoC注入方式: https://www.vojtechruzicka.com/field-dependency-injection-considered-harmful/
[2] 循环依赖: https://www.baeldung.com/circular-dependencies-in-spring
[3] 字段注入: https://blog.marcnuri.com/field-injection-is-not-recommended/
[4] IoC与DI: 概念精准解释版:http://sishuok.com/forum/blogPost/list/2427.html
通俗易懂版:https://www.zhihu.com/question/23277575/answer/169698662

史上最强Tomcat8性能优化

阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路

B2B电商平台--ChinaPay银联电子支付功能

学会Zookeeper分布式锁,让面试官对你刮目相看

SpringCloud电商秒杀微服务-Redisson分布式锁方案

查看更多好文,进入公众号--撩我--往期精彩

一只 有深度 有灵魂 的公众号0.0

猜你喜欢

转载自blog.csdn.net/a1036645146/article/details/109504106