据说,80%的人没有真正理解了Spring的依赖注入(上)

简介: 据说,80%的人没有真正理解了Spring的依赖注入(上)

前言


提起Spring,大家肯定不陌生,它是每一个Java开发者绕不过去的坎。Spring 框架为基于 java 的企业应用程序提供了一整套解决方案,方便开发人员在框架基础快速进行业务开发。


在官网中,我们发现它的核心技术之一:Dependency Injection,简称:DI ,翻译过来就是依赖注入。今天我们就来盘一盘它。


image.png


在本文中,我们将深入研究 Spring 框架 DI背后的故事,包括 Spring Inversion of Control(控制反转)、 DIApplicationContext 接口。基于这些基本概念,我们将研究如何使用基于 java 和基于 XML 的配置来 创建Spring 应用程序。最后,我们将探讨在创建 Spring 应用程序时遇到的一些常见问题,包括 bean冲突循环依赖性


一 控制反转(Inversion of Control)


在学习DI之前,我们先学习一下 IoC控制反转),接下来的一段可能读起来会让你感觉比较啰嗦,但是要细细体会每一次改变的意图,和我们的解决方案,对于理解控制反转非常重要。


首先来了解下我们通常实例化一个对象的方式。在 平时,我们使用 new 关键字实例化一个对象。例如,如果有一个 Car 类,我们可以使用以下方法实例化一个对象 Car


Car car = new Car();


因为汽车有很多零部件组成,我们定义Engine接口来模拟汽车引擎,然后将engine对象作为成员变量放在Car


public interface Engine {
     void turnOn();
}
public class Car {
    private Engine engine;
    public Car() {}
    public void start() {
        engine.turnOn();
    }
}


现在,我们可以调用start()方法吗?显然是不行的,一眼可以看出会报NullPointerException (NPE),因为我们没有在Car的构造函数中初始化engine。通常我们采用的方案就是在Car的构造函数中觉得使用Engine接口的哪个实现,并直接将该实现分配给engine字段;


现在,我们来首先创建Engine接口的实现类


public class ElectricEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("电动引擎启动");
    }
}
public class CombustionEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("燃油引擎启动");
    }
}


我们修改Car的构造函数,使用ElectricEngine实现,将我们的engine字段分配给一个实例化的ElectricEngine对象


public class Car {
    private Engine engine;
    public Car() {
        this.engine = new ElectricEngine();
    }
    public void start() {
        engine.turnOn();
    }
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}


现在我们执行start()方法,我们会看到如下输出:


电动引擎启动

燃油引擎启动


大功告成,我们成功解决了 NPE(空指针)问题,但是我们胜利了吗?哈哈哈,显然没有!


在解决问题的同时,我们又引入了另一个问题。尽管我们通过抽象Engine接口,然后通过不同的Engine实现类来负责不同类型引擎的业务逻辑,的确是很好的设计策略。但是细心的伙伴可能已经发现了,我们Car类的构造函数中将engine声明为

CombustionEngine,这将导致所有车都有一个燃油引擎。假如我们现在要创建不同的汽车对象,它有一个电动引擎,我们将不得不改变我们的设计。比较常见的方法是创建两个独立里的类,各司其职,在他们的构造函数中将engine分配给Engine接口的不同实现;


例如:


public class CombustionCar {
    private Engine engine;
    public CombustionCar() {
        this.engine = new CombustionEngine();
    }
    public void start() {
        engine.turnOn();
    }
}
public class ElectricCar {
    private Engine engine;
    public ElectricCar() {
        this.engine = new ElectricEngine();
    }
    public void start() {
        engine.turnOn();
    }
}


通过上面的一顿操作,我们成功的解决了我们引擎的问题。如果是一个日常需求,我们已经可以成功交工了。但是这显然不是我写这篇文章的目的。


从设计的角度来说,目前的代码是糟糕的,有以下两点原因:


  1. 在两个不同的类中,存在重复的start()方法;


  1. 我们需要为每个新的Engine 实现类创建一个新的类;


尤其后一个问题更加难以解决,因为我们不控制Engine的实现,随着开发人员不断的创建自己的实现类,这个问题会更加恶化;


带着上面的问题,我们继续思考…………………


我们可以创建一个父类Car,将公共代码抽取到父类中,可以轻松解决第一个问题。由于Engine字段是私有的,我们在父类Car的构造函数中接收Engine对象,并且进行赋值。


public class Car {
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void start() {
        engine.turnOn();
    }
}
public class CombustionCar extends Car{
    public CombustionCar() {
        super(new CombustionEngine());
    }
}
public class ElectricCar extends Car {
    public ElectricCar() {
        super(new ElectricEngine());
    }
}


通过这种方法,我们成功的解决了代码重复的问题,我们来测试一下:


public class Car {
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void start() {
        engine.turnOn();
    }
    public static void main(String[] args) {
        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();
    }
}


电动引擎启动  

燃油引擎启动


那么我们该如何解决我们提出的第二个问题那?


其实这个问题我们可以换个角度看:为什么我们要去关注CombustionCarElectricCar,我们现在将关注点回到我们的Car,我们现在已经允许客户端实例化Car对象时候将Engine对象作为构造函数的参数传入,其实已经消除了为每个Engine对象创建新Car的问题。因为现在Car类依赖于Engine接口,并不知道任何Engine的实现;

通过带有Engine参数的构造函数,我们已将要使用哪个Engine实现的决定从Car类本身(最初由CombustionEngine决定)更改为实例化Car类的客户端。决策过程的这种逆转称为IoC原则。现在,由客户端控制使用哪种实现,而不是由Car类本身控制使用哪种Engine实现。


有点绕,大家结合下面的示例代码,细细琢磨


public class Car {
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void start() {
        engine.turnOn();
    }
    public static void main(String[] args) {
        /**
         * 老法子
         * 为每一类型发送机的车创建类,然后实现父类car,然后在构造函数传入自己的引擎,然后调用start()
         */
        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();
        /**
         * 控制反转思想
         * 把自己看作实例化car的客户端,需要什么引擎,直接传入相关对象
         */
        CombustionEngine combustionEngine = new CombustionEngine();
        Car combustionCar = new Car(combustionEngine);
        combustionCar.start();
        ElectricEngine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.start();
    }
}


执行上面的代码,我们发现都可以获得我们想要的结果:



电动引擎启动  

燃油引擎启动


从上面的例子我们可以看到,实例化Car类的客户端可以控制所使用的Engine实现,并

且取决于将哪个Engine实现传递给Car构造函数,Car对象的行为发生巨大变化。为什么这么说,接着看下面


二 依赖注入(Dependency Injection)


在上面控制反转的知识点,我们已经解决了由谁决定使用哪种Engine实现的问题,但是不可避免,我们也更改了实例化一个Car对象的步骤;


最开始,我们实例化Car不需要参数,因为在它的构造函数里面已经为我们newEngine对象。使用IoC方法之后,我们要求在实例化一个Car之前,我们需要先创建一个Engine对象,并作为参数传递给Car构造对象。换句话说,最初,我们首先实例化Car对象,然后实例化Engine对象。但是,使用IoC之后,我们首先实例化Engine对象,然后实例化Car对象;


因此,我们在上面的过程中创建了一个依赖关系。不过这种依赖关系不是指编译时候Car类对Engine接口的依赖关系,相反,我们引入了一个运行时依赖关系。在运行时,实例化Car对象之前,必须首先实例化Engine对象。


2.1 依赖关系树


某一个具体的依赖对象大家可以理解为Spring中的bean,对于两个有依赖关系的bean,其中被依赖的那个bean,我们把它称为依赖对象


我们用图形化的方式来看看它们之间的依赖关系,其中图形的节点代表对象,箭头代表依赖关系(箭头指向依赖对象)。对于我们我的Car类,依赖关系树非常简单:


image.png


如果依赖关系树的终端结点还有自己的附加依赖关系,那么这个依赖关系树将变得更加复杂。现在再看我们上面的例子,如果CombustionEngine 还有其他依赖对象,我们首先需要创建CombustionEngine的依赖对象,然后才能实例化一个CombustionEngine对象。这样在创建Car对象时候,才能将CombustionEngine传递给Car的构造函数;


//凸轮轴        
public class Camshaft {}
//机轴
public class Crankshaft {}
public class CombustionEngine implements Engine {
  //凸轮轴
  private Camshaft camshaft;
  //机轴
  private Crankshaft crankshaft;
  public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {
      this.camshaft = camshaft;
      this.crankshaft = crankshaft;
  }
  @Override
  public void turnOn() {
      System.out.println("燃油引擎启动");
  }
}


相关文章
|
2月前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
2月前
|
Java 数据库 数据安全/隐私保护
轻松掌握Spring依赖注入:打造你的登录验证系统
本文以轻松活泼的风格,带领读者走进Spring框架中的依赖注入和登录验证的世界。通过详细的步骤和代码示例,我们从DAO层的创建到Service层的实现,再到Spring配置文件的编写,最后通过测试类验证功能,一步步构建了一个简单的登录验证系统。文章不仅提供了实用的技术指导,还以口语化和生动的语言,让学习变得不再枯燥。
56 2
|
7月前
|
XML Java 测试技术
Spring Boot中的依赖注入和控制反转
Spring Boot中的依赖注入和控制反转
|
6月前
|
设计模式 自然语言处理 Java
简单了解下Spring中的各种Aware接口实现依赖注入
在Spring框架中,Aware接口是一组用于提供特定资源或环境信息的回调接口。这些接口被设计用来允许Bean获取对Spring容器或其他相关资源的引用,并在需要时进行适当的处理。
61 2
|
6月前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
87 0
|
7月前
|
缓存 Java Spring
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
|
6月前
|
自然语言处理 Java 开发者
简单了解下Spring中的各种Aware接口实现依赖注入
【8月更文挑战第21天】在Spring框架中,Aware接口系列是一种特殊的机制,它允许Bean在初始化过程中获取到Spring容器或容器中的特定资源,从而实现了更加灵活和强大的依赖注入方式。本文将围绕Spring中的各种Aware接口,详细探讨它们如何帮助开发者在工作和学习中更好地实现依赖注入。
166 0
|
8月前
|
设计模式 Java 测试技术
Spring Boot中的依赖注入详解
Spring Boot中的依赖注入详解
|
7月前
|
XML Java 数据格式
深入理解Spring中的依赖注入原理
深入理解Spring中的依赖注入原理
|
7月前
|
Java Spring 容器
深入理解Spring Boot中的容器与依赖注入
深入理解Spring Boot中的容器与依赖注入