据说,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("燃油引擎启动");
  }
}


相关文章
|
1月前
|
开发框架 Java Spring
Spring依赖注入以及使用建议
Spring依赖注入以及使用建议
30 0
|
2月前
|
XML Java 程序员
Spring的依赖注入
Spring的依赖注入
|
2月前
|
Java Spring
Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入)
Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入)
|
3月前
|
Java API Spring
Spring6-IoC(Inversion of Control)控制反转和DI(Dependency Injection)依赖注入,手动实现IOC
Spring6-IoC(Inversion of Control)控制反转和DI(Dependency Injection)依赖注入,手动实现IOC
|
1月前
|
Java 数据库连接 API
【Spring】1、Spring 框架的基本使用【读取配置文件、IoC、依赖注入的几种方式、FactoryBean】
【Spring】1、Spring 框架的基本使用【读取配置文件、IoC、依赖注入的几种方式、FactoryBean】
49 0
|
4月前
|
XML Java 数据格式
手写spring 框架——第二篇(依赖注入)
手写spring 框架——第二篇(依赖注入)
41 0
|
2月前
|
设计模式 开发框架 Java
Spring 依赖注入方式有哪些?
Spring 依赖注入方式有哪些?
83 0
Spring 依赖注入方式有哪些?
|
2月前
|
设计模式 Java 测试技术
Spring依赖注入的魔法:深入DI的实现原理【beans 五】
Spring依赖注入的魔法:深入DI的实现原理【beans 五】
124 0
|
2月前
|
安全 Java 开发者
Spring依赖注入大揭秘:@Autowired、@Qualifier和@Resource的区别与应用
Spring依赖注入大揭秘:@Autowired、@Qualifier和@Resource的区别与应用
55 0
|
4月前
|
XML Java 开发者
Spring框架: 什么是依赖注入(Dependency Injection)?
Spring框架: 什么是依赖注入(Dependency Injection)?
63 2