Advertisement

这几个关于Spring 依赖注入的问题你清楚吗?

阅读量:

前言

本章的主要内容是围绕我们在Spring 开发过程中涉及的几个关键知识点展开深入分析。为了帮助读者更好地理解这些概念,建议他们先了解以下相关问题:

@Autowired, @Resource, @Inject这三个注解各自的作用是什么
在使用@Autowired时, 是否曾遇到Field injection is not recommended的警告信息?你是否了解背后原因
Spring提供了哪些依赖注入的方式?官方推荐优先采用哪种方法
如果你已经掌握了这些知识要点,则你的开发能力应该算是不错了

下面我们就依次对上述问题进行解答,并且总结知识点。

@Autowired,@Resource,@Inject 三个注解的区别

采用@Autowired@Resource以及@Inject三种注解进行依赖注入是Spring框架的核心特性之一。接下来将详细阐述这些注解之间的主要区别。

@Autowired

@Autowired是用于实现Spring 框架属性注入功能的注解,在开发时必须包含相应的导入语句以确保类被正确引用。

这里先给出一个示例代码,方便讲解说明:

复制代码
    public interface Svc {
    
    void sayHello();
    }
    
    @Service
    public class SvcA implements Svc {
    
    @Override
    public void sayHello() {
        System.out.println("hello, this is service A");
    }
    
    }
    
    @Service
    public class SvcB implements Svc {
    
    @Override
    public void sayHello() {
        System.out.println("hello, this is service B");
    }
    
    }
    
    @Service
    public class SvcC implements Svc {
    
    @Override
    public void sayHello() {
        System.out.println("hello, this is service C");
    }
    }

测试类:

复制代码
    @SpringBootTest
    public class SimpleTest {
    
    @Autowired
    // @Qualifier("svcA")
    Svc svc;
    
    @Test
    void rc() {
        Assertions.assertNotNull(svc);
        svc.sayHello();
    }
    
    }

装配顺序:

1.按照type在上下文中查找匹配的bean

查找type为Svc的bean

2.如果有多个bean,则按照name进行匹配

  1. 如果有@Qualifier注解,则按照@Qualifier指定的name进行匹配

查找name为svcA的bean

  1. 如果没有,则按照变量名进行匹配

查找name为svc的bean

  1. 如果无法匹配,则会触发错误。(@Autowired(required=false), 如果设置required为false(默认情况下要求属性必须被注入required=true),则避免了注入失败导致的异常抛出)

@Inject

在Spring框架中,在这种环境下,@Inject与Autowired等价,并且它们都采用AutowiredAnnotationBeanPostProcessor作为依赖注入的方式进行操作。

在这里插入图片描述

@Inject是 JSR-330 定义的规范,如果使用这种方式,切换到Guice也是可以的。

Guice 是 google 开源的轻量级 DI 框架

如果说要区分这两个概念时,首先要注意的是@Inject位于Java EE组件库中,并且在SE(Spring Environment)环境中需要特别注意地引入。另一个关键区别在于,在@Inject中设置required参数为false时会与普通的injected对象产生不同的行为。

@Resource

该注解基于JSR-250标准进行定义。该框架在CommonAnnotationBeanPostProcessor中具备管理所有相关注解的能力,并特别支持使用@Resource标记。

在这里插入图片描述

具有重要功能的是@Resource注解中的name与type两个属性。其中Spring通过将@Resource注解中的name属性解析为Bean命名字段,并以type属性指定Bean所属类型。同时通过type属性确定Bean所属类型。

装配顺序:
  1. 当同时设置name和type时,在Spring环境中精确对应唯一的Bean以完成集成。
  2. 当指定名称时,在环境中搜索对应的Bean(通过ID)完成集成。
  3. 当设置类型时,在环境中找到与之相符合的单一Bean以完成集成。
  4. 若未设定任何键值对,默认依据名称法集成;若无Bean满足该条件则转而采用类型法。

IDEA 提示

Field injection is not recommended

在Spring开发过程中使用IDEA进行配置时,在属性上应用@Autowired注解时会发现IDEA会发出警告提示:

inspection information

inspection info: spring team advises: "your beans should utilize constructor-based dependency injection. always use assertions to verify necessary dependencies."

在这里插入图片描述

翻译过来就是这个意思:

Spring 开发团队的建议是:在你的Spring Bean严格遵循基于构造器的方式进行依赖注入,并且对于关键依赖必须采用断言机制进行验证。

比如如下代码:

复制代码
    @Service
    public class HelpService {
    @Autowired
    @Qualifier("svcB")
    private Svc svc;
    
    public void sayHello() {
        svc.sayHello();
    }
    }
    
    public interface Svc {
    void sayHello();
    }
    
    @Service
    public class SvcB implements Svc {
    @Override
    public void sayHello() {
        System.out.println("hello, this is service B");
    }
    }

定位到$ injection@ constructor处时,在代码编辑器中按下Alt + Enter键组合键快速完成修改操作后,则会将代码转换为基于Constructor的注入方式。

复制代码
    @Service
    public class HelpService {
    private final Svc svc;
    
    @Autowired
    public HelpService(@Qualifier("svcB") Svc svc) {
        // Assert.notNull(svc, "svc must not be null");
        this.svc = svc;
    }
    
    public void sayHello() {
        svc.sayHello();
    }
    }

如果遵循Spring官方指南,在svc作为必要依赖项的情况下,则应调用Assert.notNull(svc, "svc必须不是null")以实现验证逻辑。

虽然修正这个警告提示相对容易,
我认为理解Spring团队为什么要提出这样的建议显得尤为重要。
直接采用基于字段的注入方式存在哪些问题?

首先我们需要知道,Spring 中有这么3种依赖注入的方式:

  • 基于字段注入(属性注入)
  • 基于设置器注入
  • 基于构造函数注入(构造器注入)

基于 field 注入

即为在 bean 的字段上使用注解实现依赖注入的技术,在 Java 中可以通过反射机制直接将对象实例注入到字段中。这也是一种常用且习惯用法的技术实现方式之一,但并非 recommended by Spring 开发团队。例如:

@Autowired
private Svc svc;

2. 基于 setter 方法注入

通过对应变量调用setXXX()操作符,并在函数体内适当位置插入注解语句来实现依赖注入机制。例如:

复制代码
    private Helper helper;
    
    @Autowired
    public void setHelper(Helper helper) {
    this.helper = helper;
    }

自Spring 4.3版本起,在setter 后方的Autowired注解无需再添加。

3. 基于 constructor 注入

通过将所有必要的依赖归入带有注解的构造函数参数,并在构造函数中设置对应变量的值,
这种做法就是基于构造函数的注入方式。

复制代码
    private final Svc svc;
    
    @Autowired
    public HelpService(@Qualifier("svcB") Svc svc) {
    this.svc = svc;
    }

在从Spring框架4.3版本起的情况下,在一个类仅定义了一个构造函数时,则该构造函数无需携带@Autowired标记

基于 field 注入的好处

正如你所示的那样, 这种方案相当简洁, 代码显得相当简单, 容易理解。你的类能够专注于核心业务, 并不受外部依赖注入的影响。具体操作很简单, 只需将Autowired直接放置在变量上即可, 靠陷注入框架会自动提供所需组件, 无需使用自定义构造器或set方法

基于 field 注入的坏处

成也萧何败也萧何

基于 field 注入的方法虽然简便,然而这些缺陷在实际开发中经常暴露出来。这些问题我在日常开发过程中也会不时遇到。

容易违背了单一职责原则

采用基于field注入的方法进行代码实现会显得异常直接。即便一个类拥有十几个依赖项,在这种情况下你可能也觉得没有问题。然而,在采用构造器注入的方式时,在某一特定阶段会出现参数过于繁多的情况——这会让人明显感到存在问题。太多的依赖通常意味着该类必须承担更多的责任——这明显违背了单一职责原则(SRP:Single responsibility principle)。

这个问题在我司的项目代码真的很常见。

依赖注入与容器本身耦合

因此,在设计时应确保受容器管理的对象不应依存于容器内部使用的外部依赖。另一种说法是这类对象应具备简单性,并且能够实现单例化的同时自给自足地满足自身需求。

这个问题具体可以表现在:

你的类深度依赖(strongly coupled)于其配置管理工具(dependency container),无法脱离它独立运行。你的类实现对象实例化时必须依靠现有的依赖管理机制(dependency container),如在单元测试场景中绕过反射进行操作会受限。这种做法更接近于系统集成测试而非单独的单元测试

不能使用属性注入的方式构建不可变对象(final修饰的变量)

如果遇见问题都可以来一起探讨:https://shimo.im/docs/VqQR6tPrpR3C3tjq/

Spring 开发团队的建议

Given that you can integrate constructor-based and setter-based dependency injection (DI), it is a best practice to prefer using constructors for mandatory dependencies and setting methods or configuration methods where optional dependencies are needed.

简单来说,就是

  • 强制依赖就用构造器方式
  • 可选、可变的依赖就用setter 注入

当然,在同一类中进行操作时可以同时应用这两种方法。基于强制性原则的构造器注入更适合这种场景;而基于可变性原理的Setter 注入则更适合另一种情况。

让我们看看Spring 这样推荐的理由,首先是基于构造方法注入,

Generally, The Spring team endorses constructor injection because it allows application components to be implemented as immutable objects and ensures that required dependencies are not null. Additionally, constructor-injected components are always returned by the client(calling) code in a fully initialized state. I noteworthy remark is that 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.

Spring 团队推荐采用基于构造模式的依赖注入技术。该模式具有两个显著特点:一方面能够将依赖注入到不可变对象中(借助final修饰),从而避免其被后续操作修改;另一方面也能够保证这些对象不会初始化为null值。此外,在调用时这些组件已经预先完成了所有必要的配置与初始化工作。与此同时,在软件质量角度来看,在单个类中过度复杂的构造方法通常会引入代码异味问题(Code Smell),这可能导致该类承担过多责任或职责。

而对于基于 setter 的注入,他们是这么说的:

Setters are typically employed exclusively for optional dependencies capable of assuming reasonable default values within a class structure. Unless otherwise specified, not-null validations are necessary wherever the code interacts with such dependencies. A key advantage of setter injection lies in its ability to facilitate reconfiguration or subsequent re-injection of class objects through setter methods.

基于 setter 的方式,则只适合用于注入非必需的依赖,并应在类中为该依赖提供一个合理的默认值。若采用 setter 注入必需的依赖,则将导致过多的 null 检查出现在代码中。采用 setter 进行注入的一个优点在于:该依赖可以非常方便地进行修改或重新注入。

小结

本篇文章主要介绍了 Spring 的依赖注入技术及其在实际开发中的应用情况。通过阅读后期待能够进一步理解这一技术。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力

全部评论 (0)

还没有任何评论哟~