关于后端:Spring-的依赖注入DI

3次阅读

共计 8431 个字符,预计需要花费 22 分钟才能阅读完成。

前言

欢送来到本篇文章,书接上回,本篇说说 Spring 中的依赖注入,包含注入的形式,写法,该抉择哪个注入形式以及可能呈现的循环依赖问题等内容。

如果正在浏览的敌人还不分明什么是「依赖」,倡议先看看我第一篇文章,通过 Employee 和 Department 简略说了什么是所谓的依赖。

什么是依赖注入?

依赖注入是一个过程,举个例子:

public class A {
    private B b;
    // 省略 getter 和 setter
    // 省略构造方法
}

当初 A 类 是依赖 B 类的,没有 B,A 什么都不是。Spring IoC 容器创立好 B 的实例对象后并赋值给 A 对象中的 b 属性(成员变量)的过程,就是所谓的「依赖注入」。

当然,这也是所谓的 管制反转 了,对象 b 的创立不是咱们手动 new 创立的,而是 Spring IoC 容器创立的。

应用 DI 准则,能够让代码更加简洁,当对象提供其依赖项时,解耦也更加无效。依赖项 B 注入给 A 的实例对象,A 的实例对象是不会查找它的依赖项 B 的,也不晓得依赖项 B 的地位。

依赖注入次要有 两种 实现形式:基于构造方法的依赖注入 基于 Setter 的依赖注入

基于构造方法的依赖注入

基于构造方法的 DI 是通过 Spring IoC 容器调用带有许多参数的构造方法来实现的,每个参数示意一个依赖项。这与上一篇中调用具备特定参数的动态工厂办法来结构 Bean 是简直等价的。

public class Vehicle {
    
    // Vehicle 依赖于 Producer 对象,或者说 Vehicle 持有一个 Producer 依赖项
    private final Producer producer;
    
    // 构造方法,让 Spring IoC 容器可能注入 Producer 对象给 Vehicle 对象
    public Vehicle(Producer producer) {this.producer = producer;}
}

留神,这个 Vehicle 类就只是一个一般的 Java Bean,或者说 POJO,没什么特别之处,最一般不过的类了,没有继承或者实现 Spring 指定的任意类。

构造方法中参数的解析

Spring IoC 是通过构造方法参数的解析来匹配须要注入的依赖项。换句话说,解析实际上就是通过匹配参数的类型,来确定注入什么依赖项

如果构造方法的参数中没有存在潜在的歧义,那么在 Bean 被实例化的时候,构造方法中参数的程序和实例化时进行赋值是一样的。

package cn.god23bin.demo.model;

public class A {
    private B b;
    private C c;
    
    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }
}

独自写这些类,是实现不了依赖注入的,这时候须要配置元数据了,让它与 Java 类相结合,能力施展 Spring IoC 的作用。

当初假如 B 和 C 是没有任何继承上的关联,也没有任何潜在的歧义,那么咱们在配置元数据中的配置是失常的,须要应用到 <constructor-arg/> 标签,该标签只需有一个 ref 属性,即援用(reference),指明 A 的构造方法的参数为 B 和 C,如下:

<beans>
    <bean id="a" class="cn.god23bin.demo.model.A">
        <constructor-arg ref="b"/>
        <constructor-arg ref="c"/>
    </bean>

    <bean id="b" class="cn.god23bin.demo.model.B"/>

    <bean id="c" class="cn.god23bin.demo.model.C"/>
</beans>

咱们晓得,B 和 C 是属于援用类型,类型是确切的,所以 间接应用 ref 属性

如果是根本数据类型,那么赋值时,就可能会有歧义 ,Spring 不能确定这个值的类型,比方一个根本数据类型的值是 true 这时候歧义就呈现了,它是布尔类型还是字符串类型呢 ?这就须要由咱们来通知 Spring 了,就 须要应用 typevalue 属性来指定

比方:

package cn.god23bin.demo.model;

public class D {
    private final int money;
    private final String power;
    
    public D(int money, String power) {
        this.money = money;
        this.power = power;
    }
}
<bean id="d" class="cn.god23bin.demo.model.D">
    <constructor-arg type="int" value="25000000"/>
    <constructor-arg type="java.lang.String" value="25"/>
</bean>

当然,除了应用 type 指定根本数据类型,还有两个属性能够解决歧义,别离是 indexname 属性。

index 属性:

应用 index 属性指定构造方法参数的下标,下标是从 0 开始的,来解决参数类型呈现歧义的状况。

<bean id="d" class="cn.god23bin.demo.model.D">
    <constructor-arg index="0" value="25000000"/>
    <constructor-arg index="1" value="25"/>
</bean>

name 属性:

应用 name 属性指定构造方法参数的名称来解决歧义。

<bean id="d" class="cn.god23bin.demo.model.D">
    <constructor-arg name="money" value="25000000"/>
    <constructor-arg name="power" value="25"/>
</bean>

基于 Setter 的依赖注入

基于 Setter 的 DI,是在 Spring IoC 容器调用了 Bean 的无参构造方法或者无参的动态工厂办法实例化了 Bean 之后,再来调用 Bean 的 Setter 办法来实现的。

上面这个仍旧是一般的 Java Bean,当然你加上相干业务逻辑,就不是纯正的 Java Bean 了,不过 Spring 仍旧可能治理这种对象。

Vehicle:

public class Vehicle {
    
    // Vehicle 持有的 Producer 依赖项
    private Producer pro;
    
    // 同理
    private Author aut;
    
    // 同理
    private int money;
    
    // setter 办法,能够让 Spring IoC 容器调用注入 Producer 对象
    public void setProducer(Producer pro) {this.pro = pro;}
    
    // 同理
    public void setAut(Author aut) {this.aut = aut;}
    
    // 同理
    public void setMoney(int money) {this.money = money}
    
    // 业务逻辑,无关 Producer 的...
}

对应的配置元数据:

<bean id="vehicle" class="cn.god23bin.demo.model.Vehicle">
    <!-- 应用 property 标签的 ref 属性注入援用类型的依赖项 -->
    <property name="pro" ref="producer"/>
    <!-- 应用内嵌 ref 标签 注入 -->
    <property name="aut">
        <ref bean="author"/>
    </property>
    <property name="money" value="255"/>
</bean>

<bean id="producer" class="cn.god23bin.demo.model.Producer"/>
<bean id="author" class="cn.god23bin.demo.model.Author"/>

Spring IoC 容器(ApplicationContext)反对基于构造方法的 DI 和基于 Setter 的 DI,也反对应用构造方法注入了一部分依赖项后,再应用 Setter 的形式注入其余的依赖项。

咱们能够通过配置一个 BeanDefinitionPropertyEditor 来实现这些属性的注入。然而,咱们根本不会这样用,而是应用 XML 的 Bean 定义,应用注解的 Bean 定义(比方 @Component@Controller 等),又或者应用 @Bean 这种基于 Java 配置类的形式(@Configuration)来定义。

实际上,这些最终都会转成 BeanDefinition 对象的并被 Spring IoC 应用。

抉择哪个 DI 形式?

在代码的编写中,抉择应用构造方法注入还是应用 Setter 注入呢?

官网上是这么说的:

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

简而言之,Spring 团队是 举荐用构造方法来实现 DI 的

  • 对于 强依赖项 ,间接用基于构造方法的 DI,这种注入形式 可能保障注入的 Bean 对象不可变,并且确保须要的依赖不为空 。此外,构造方法注入的依赖总是可能在返回 客户端(调用方)的时候保障齐全初始化的状态。
  • 对于可选的 弱依赖项,应用基于 Setter 的 DI。当应用 @Autowired 注解,并将注解用到 Setter 办法上的话,那么这个 Setter 设置的属性就变成强依赖了。

对于构造方法进行 DI,其中:

  • 依赖不可变:其实说的就是 final 关键字。
  • 依赖不为空:省去了咱们对注入的 Bean 的查看,当要实例化 Bean 的时候,因为本人实现了有参数的构造方法,所以不会调用默认构造方法,那么就须要 Spring IoC 容器传入所须要的参数给有参构造方法,所以就两种状况,有该类型的参数传入,那么 OK;无该类型的参数,那么报错。
  • 齐全初始化的状态:这个能够跟下面的依赖不为空联合起来,向构造方法传参之前,要确保注入的内容不为空,那么必定要调用依赖对象的构造方法实现实例化。而在 Java 类加载实例化的过程中,构造方法是最初一步才执行的,所以返回来的实例化对象都是齐全初始化之后的状态。

依赖的处理过程

Spring IoC 容器按如下形式执行 Bean 的依赖解决(Dependency Resolution Process,集体认为把 Resolution 了解成解决,解决等意思比拟好)。

  • 依据配置元数据的内容,ApplicationContext 被创立和初始化。这个配置元数据是用来形容所有 Bean 的,它能够是 XML、Java 代码或注解。
  • 对于每个 Bean 来说,它的依赖是以属性、构造方法参数或动态工厂办法的参数(如果你用它代替构造方法)的模式表白的。在 Spring IoC 创立 Bean 的时候,这些依赖会被提供给须要的 Bean。
  • 每个属性或构造方法参数都是要设置的值的理论定义,或对容器中另一个 Bean 的援用。
  • 每个作为值的属性或构造方法参数都会从其指定格局转换为该属性或构造方法参数的理论类型。默认状况下,Spring 能够将以字符串格局提供的值转换为所有内置类型,如 intlongStringboolean 等等。

当 Spring IoC 容器被创立时,Spring IoC 容器一开始就会校验每个 Bean 的配置。在创立 Bean 之前,Bean 的属性自身不会被设置。

而且当容器被创立时,那些具备 单例作用域 的 Bean 也默认会被创立。对于其余状况的 Bean,只有在被申请时才会创立。

一个 Bean 的创立是有可能导致一堆 Bean 被创立,这一堆的 Bean 被称为 Bean 图(a graph of beans),因为 Bean 与 Bean 之间可能存在相互依赖,如同社会中的人一样,都有相干的分割,所以造成 Bean 图。

Bean 作用域在进行 Bean 定义的时候能够进行定义,应用 scope 属性即可定义 Bean 的作用域。

循环依赖

当你应用构造函数注入的时候,某种状况下可能产生一个无奈解决的循环依赖问题。那什么是循环依赖

举个例子:

public class A {
    private B b;
    
    public A(B b) {this.b = b;}
}
public class B {
    private A a;
    
    public B(A a){this.a = a;}
}

如上所示,A 类通过构造方法注入须要 B 类的对象,而 B 类也通过构造方法注入 A 类的一个对象,这种状况就是所谓的 循环依赖,A 和 B 之间相互依赖。

A 和 B 之间的循环依赖关系会迫使其中一个 Bean 在未齐全初始化之前,被注入到另一个 Bean 中。

这时候,Spring IoC 容器会在运行时检测到这种循环的援用,并抛出一个 BeanCurrentlyInCreationException

有一种解决方案就是:批改 A 类或者 B 类的源代码,使其通过 Setter 形式注入依赖项。

当然,个别状况下,咱们能够齐全置信 Spring,Spring 会在容器加载时检测配置问题,如查看援用不存在的 Bean 或循环依赖等。它会尽量晚地设置 Bean 的属性以及解决依赖的关系。

假如当你申请一个对象时,如果在创立该对象或其依赖关系时呈现问题,曾经正确加载的 Spring 容器就会产生异样。所以,这意味着咱们须要提前发现这些问题。

默认状况下,ApplicationContext 会事后实例化单例 Bean,所以在创立 Spring IoC 容器时会破费一些工夫和内存。但益处是能够在容器创立时发现配置问题,而不是在后续呈现。

如果不存在循环依赖关系,一个 Bean 在被注入到另一个 Bean 之前会被齐全配置。这意味着当 A 依赖于 B 时,Spring IoC 容器会先齐全配置好 B(包含实例化 Bean、设置依赖关系和调用相干的生命周期办法),再调用 A 的 Setter 办法。

依赖注入示例

上面写下示例,疾速回顾下下面的两种注入形式,帮忙大家更好了解在以 XML 作为配置元数据的状况下,应用基于构造方法的 DI 和基于 Setter 的 DI。

  1. 基于构造方法的 DI:
package cn.god23bin.demo.service;

import cn.god23bin.demo.repository.UserRepository;

public class UserService {
    
    // UserService 的依赖项 UserRepository
    private UserRepository userRepository;

    // 应用构造方法进行 DI,将 UserRepository 注入到 UserService
    public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
    
}
<bean id="userService" class="cn.god23bin.demo.service.UserService">
   <constructor-arg ref="userRepository" />
</bean>

<bean id="userRepository" class="cn.god23bin.demo.repository.UserRepository" />
  1. 基于 Setter 的 DI:
package cn.god23bin.demo.service;

import cn.god23bin.demo.repository.UserRepository;

public class UserService {
    
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {this.userRepository = userRepository;}
    
}
<bean id="userService" class="cn.god23bin.demo.service.UserService">
   <property name="userRepository" ref="userRepository" />
</bean>

<bean id="userRepository" class="cn.god23bin.demo.repository.UserRepository" />

总结

本文结尾举了个例子讲了什么是依赖注入,实际上就是一个过程,把一个 Bean 注入到另一个 Bean 中的过程。

这个过程,在 Spring 中有两种形式来实现,一种是基于构造方法的 DI,另一种是基于 Setter 的 DI。当然,网上讲到的依赖注入,还有好几种形式,目前对于咱们来说,晓得这两种就 OK。

对于这两种 DI,咱们也说了如何抉择,具体看官网的说法,举荐的是基于构造方法的 DI,当然,具体问题具体分析,有时候是须要用到基于 Setter 的 DI 的,比方解决循环依赖的时候。

接着也说了依赖的处理过程,简略点就是 Spring 会先依据配置元数据去创立 ApplicationContext 这个作为 Spring IoC 容器,在容器创立时,会验证 Bean 是否正确配置了,默认单例的 Bean 会先被创立等等等的解决。同时可能遇到循环依赖的问题,一种解决方案就是将某一个 Bean 应用 Setter 的形式注入依赖项。

以上,便是本篇文章的内容了。下期咱们讲讲依赖注入的一些细节写法,比方有的依赖项是汇合,那么配置元数据中该如何写这个汇合,进而实现汇合的依赖注入等等。

心愿本篇有帮忙到大家,有趣味的话能够关注我,关注这个系列!

最初的最初

心愿各位屏幕前的 靓仔靓女们 给个三连!你轻轻地点了个赞,那将在我的心里世界削减一颗亮堂而夺目的星!

咱们下期再见!

正文完
 0