一、引言

在现代软件开发中,Spring 框架已经成为构建企业级应用的首选工具,而在这其中,IoC(控制反转)和 DI(依赖注入)是简化开发过程、提升代码灵活性和可维护性的关键利器。它们不仅解放了开发者的双手,还为系统架构的可扩展性和模块化提供了坚实的基础。

这篇博客将带你深入探索 Spring 框架中 IoC 和 DI 的核心概念,从理论到实践,逐步揭示它们如何将繁琐的依赖管理转化为优雅的代码架构。无论你是刚接触 Spring 的新手,还是希望深入了解其背后原理的资深开发者,这篇文章都将为你提供独特的视角和实用的指导。让我们一起揭开这两个强大工具的神秘面纱,看看它们如何成为简化 Spring 应用开发的秘密武器。


二、控制反转 IoC

2.1、什么是 IoC

a)IoC 的定义与原理

大家都说 Spring 是包含了众多工具方法的 IoC 容器,那究竟什么是 IoC 容器呢?首先得理解容器这个词的意思,容器就是装某个东西的东西,举个例子,杯子是水的容器,好比我们之前学的 Tomcat,它是一个 Web 容器,Map、List 等集合,是一个数据存储的容器。

IoC 是Spring 的核心思想,我们之前其实就接触过 IoC 了,比如写代码的时候用到的五大注解,如:@RestController @Controller 这些,就是把我们写的这些类交给 Spring 统一进行管理,Spring 框架启动时就会加载该类,这种思想就是 IoC 思想。而 IoC 这种思想有个专业术语叫 "控制反转"。很好理解嘛,字面意思,本来控制权是我们自己的,但是通过注解的方式,把控制权交给 Spring 管理,我们自己就不管理了,就不用每次使用的时候 new 对象了,Spring 在项目启动的时候就给我们 new 好了,我们直接用就行,有点饿汉模式内味儿了哈。

b)传统对象创建与管理 vs IoC

再使用代码说明一下,假设我们要造一辆汽车,这辆汽车是不是依赖于车身,然后车身依赖于底盘,底盘又依赖于轮子,那我们的代码就这样诞生了,如下:

public class BlogTest {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}
// 车子
class Car {
    private Framework framework;
    public Car() {
        framework = new Framework();
    }
    public void run() {
        System.out.println("cat run...");
    }
}
// 车身
class Framework {
    private Bottom bottom;
    public Framework() {
        bottom = new Bottom();
        System.out.println("Framework init...");
    }
}
// 底盘
class Bottom {
    private Tire tire;
    public Bottom() {
        tire = new Tire();
        System.out.println("Bottom init...");
    }
}
// 轮胎
class Tire {
    private Integer size;// 轮胎尺寸
    public Tire() {
        this.size = 17;
        System.out.println("轮胎尺⼨:" + size);
    }
}

这是运行结果:IoC 与 DI : 简化 Spring 开发的秘密武器_控制反转

虽然说这样子我们看着没问题,但是假设说我们需要自己定义轮子的大小呢?那代码就得改成这样了:

public class BlogTest {
    public static void main(String[] args) {
        Car car = new Car(19);
        car.run();
    }
}
// 车子
class Car {
    private Framework framework;
    public Car(int size) {
        framework = new Framework(size);
    }
    public void run() {
        System.out.println("cat run...");
    }
}
// 车身
class Framework {
    private Bottom bottom;
    public Framework(int size) {
        bottom = new Bottom(size);
        System.out.println("Framework init...");
    }
}
// 底盘
class Bottom {
    private Tire tire;
    public Bottom(int size) {
        tire = new Tire(size);
        System.out.println("Bottom init...");
    }
}
// 轮胎
class Tire {
    private Integer size;// 轮胎尺寸
    public Tire(int size) {
        this.size = size;
        System.out.println("轮胎尺⼨:" + this.size);
    }
}

大家也看到了,耦合度非常的高,那既然这样,我们不妨换个思路,直接就让它们先创建好,然后我们直接使用,再通俗一点就是,你先造好配件,然后我直接用就行,如下:

public class BlogTest {
    public static void main(String[] args) {
        Tire tire = new Tire(10);
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}
// 车子
class Car {
    private Framework framework;
    public Car(Framework framework) {
        this.framework = framework;
    }
    public void run() {
        System.out.println("cat run...");
    }
}
// 车身
class Framework {
    private Bottom bottom;
    public Framework(Bottom bottom) {
        this.bottom = bottom;
        System.out.println("Framework init...");
    }
}
// 底盘
class Bottom {
    private Tire tire;
    public Bottom(Tire tire) {
        this.tire = tire;
        System.out.println("Bottom init...");
    }
}
// 轮胎
class Tire {
    private Integer size;// 轮胎尺寸
    public Tire(int size) {
        this.size = size;
        System.out.println("轮胎尺⼨:" + this.size);
    }
}

那这样的耦合度是不是就降低了呢?这样的思想就是控制反转。

在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire

改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car

用一张图表示就是:

IoC 与 DI : 简化 Spring 开发的秘密武器_SpringBoot_02

我们发现了⼀个规律,通⽤程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了 Framework,Framework 创建并创建了 Bottom,依次往下,而改进之后的控制权发⽣的反转,不再是使⽤⽅对象创建并控制依赖对象了,而是把依赖对象注⼊将当前对象中,依赖对象的控制权不再由当前类控制了,这样的话,即使依赖类发⽣任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。

2.2、IoC 的优点

  • 提高代码的模块化和可维护性:通过将对象的创建和管理职责交给 IoC 容器,可以使代码更加模块化。
  • 降低类之间的耦合度:减少类与类之间的直接依赖,提高代码的灵活性。
  • 增强测试性:通过依赖注入,可以方便地对类进行单元测试。

三、依赖注入 DI

3.1、什么是 DI

其实 IoC 和 DI 是从不同的角度的描述的同⼀件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦。说的通俗点就是 IoC 是把对象交给 Spring 管理,就是往这个容器里面存东西,而 DI 呢就是从这个容器里面把取出来。所以 DI 是实现 IoC 的一种具体方式。

3.1、DI 的实现

  • Spring 框架中如何实现 DI:Spring 通过其 IoC 容器实现依赖注入,支持多种配置方式。
  • XML 配置 vs 注解配置 vs Java 配置:Spring 提供了多种配置方式来实现依赖注入,包括 XML 配置文件、注解和 Java 配置类。

四、IoC & DI 使用

我们知道了控制反转和依赖注入,那我们又该怎样使用呢?控制反转的方式是使用五大注解和 @Bean,DI 又分三种,分别是构造方法注入,Setter方法注入,字段注入,接口注入,方法注入。这里只详细讲解字段注入,这也是最常用的方式。

4.1、如何把控制权交给 Spring ?

a)类处理方式

类的话主要是使用五大注解,分别是:@Controller@Service@Repository@Component@Configuration。那这些注解有什么区别呢?如下:

  • @Controller:控制层,接收请求,对请求进行处理,并进行响应。
  • @Servie:业务逻辑层,处理具体的业务逻辑。
  • @Repository:数据访问层,也称为持久层,负责数据访问操作。
  • @Configuration:配置层,处理项目中的⼀些配置信息。

代码演示如下:

@Service
public class BlogTest {
    public void test() {
        System.out.println("Service Run...");
    }
}

这样我们就存好了,那该怎么取出来呢?欸,这就要使用 ApplicationContext 里面的 getBean 方法了(注意导包,是这个 import org.springframework.context.ApplicationContext;),也就是我们启动 web 那个程序的方法,代码如下:

@SpringBootApplication
public class BeanTestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
        context.getBean(BlogTest.class).test();
    }
}

此时我是完全没有 new 对象的哈,完全就是从 Spring 里面拿的,其他的主页使用方式也是一样的,改一下注解名字就行了。

b)方法处理方式

方法的话主要是使用 @Bean 来声明,代码如下,为了能更直观的看到,要稍作修改代码:

@Service
public class BlogTest {
    @Bean
    public String test() {
        return "BlogTest Run...";
    }
}
@SpringBootApplication
public class BeanTestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
        String s = (String) context.getBean("test");
        System.out.println(s);
    }
}

运行结果如图:IoC 与 DI : 简化 Spring 开发的秘密武器_控制反转_03,除了这种方法,我们还可以使用上面那种找类的方式,直接在 Spring 里面找哪个方法是返回 String 就行了,代码如下:

@SpringBootApplication
public class BeanTestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
        String s = context.getBean(String.class);
        System.out.println(s);
    }
}

可是这种方式,在我们定义多个返回值相同类型的时候就会出问题,比如这样:

@Service
public class BlogTest {
    @Bean
    public String test1() {
        return "BlogTest1 Run...";
    }
    
    @Bean
    public String test2() {
        return "BlogTest2 Run...";
    }
}

这是错误日志:

IoC 与 DI : 简化 Spring 开发的秘密武器_控制反转_04

它会说找到了两个合适的 bean,分别是 test1 和 test2,这就起冲突了,这时候我们就可以指定找 bean 的名字了,可以直接就是方法名,也可以自己重新定义一个,然后使用重新定义的名字找,像上面的 (String) context.getBean("test"); 这种就是根据方法名去找的,这里就不过多演示,重命名是这样的:

@Service
public class BlogTest {
    @Bean(name = "t1")// 这是正常写
    public String test1() {
        return "BlogTest1 Run...";
    }

    @Bean("t2")// 这是简写
    public String test2() {
        return "BlogTest2 Run...";
    }
}
@SpringBootApplication
public class BeanTestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
        String s1 = (String) context.getBean("t1");
        System.out.println(s1);
        String s2 = (String) context.getBean("t2");
        System.out.println(s2);
    }
}

运行结果:IoC 与 DI : 简化 Spring 开发的秘密武器_SpringBoot_05

但是有个问题,在通过在把方法写成重载之后,好像每次找到的都是参数最多的那一个(通过本人实验得知,不一定正确,可能和环境有关),代码如下:

@Service
public class BlogTest {
    // 先交给 Spring 一些参数,到时候好拿
    @Bean
    public String name1() {
        return "name1";
    }
    @Bean
    public String name2() {
        return "name2";
    }
    @Bean
    public String name3() {
        return "name3";
    }
    @Bean
    public String name4() {
        return "name4";
    }
    @Bean
    public String name5() {
        return "name5";
    }
    
    @Bean
    public String test() {
        return "BlogTest1 Run...";
    }
    @Bean
    public String test(String name1) {
        return "BlogTest2 Run...";
    }
    @Bean
    public String test(String name1, String name2) {
        return "BlogTest3 Run...";
    }
    @Bean
    public String test(String name1,String name2, String name3) {
        return "BlogTest4 Run...";
    }
    @Bean
    public String test(String name1, String name2, String name3, String name4) {
        return "BlogTest5 Run...";
    }
    @Bean
    public String test(String name1, String name2, String name3, String name4, String name5) {
        return "BlogTest6 Run...";
    }
}

这是运行结果,IoC 与 DI : 简化 Spring 开发的秘密武器_控制反转_06,不管我如何更换这几个重载方法的位置,每次都是这个结果。

4.2、扫描路径

前面我们说到了使用五大注解和 Bean 去给 Spring 声明,那问题来了,只要声明了,那就一定会生效吗?答案是不一定,得看它能不能扫描得到,我们可以通过修改项目工程的目录结构,来测试 bean 对象是否生效:

IoC 与 DI : 简化 Spring 开发的秘密武器_SpringBoot_07

当我把这个类移动到这个包里面时,再次运行就变成了这样:

IoC 与 DI : 简化 Spring 开发的秘密武器_控制反转_08

这样它就找不到了,为什么没有找到 bean 对象呢?使用五大注解声明的 bean,要想生效,还需要配置扫描路径,让 Spring 扫描到这些注解,也就是通过 @ComponentScan 来配置扫描路径。配置你要在哪个路径下扫描,如下:

@SpringBootApplication
@ComponentScan({"com.zmbdp.beantest"})
public class BeanTestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
        String s = (String) context.getBean("test");
        System.out.println(s);
    }
}

这样就运行成功了。

4.3、如何取出来

存钱现在讲完了,那我们该如何取钱呢?总不能每次都依赖 ApplicationContext 进行扫描吧?那也太麻烦了,这里我就主要介绍通过注解得方式注入依赖,这个注解是 @Autowired,代码如下:

@Data// 这个注解会自动帮我们写好 get、set 和 toString 方法
@Configuration
public class UserInfo {
    private Integer id;
    private String name;
}
@RestController
@RequestMapping("/blogTest")
public class BlogTest {
    @Autowired
    private UserInfo userInfo;
    @RequestMapping("/setUser")
    public String setUser() {
        userInfo.setId(19);
        userInfo.setName("zhangsan");
        return userInfo.toString();
    }
}

然后,我们访问这个页面时:IoC 与 DI : 简化 Spring 开发的秘密武器_依赖注入_09,这就是依赖注入。但是当多个 bean 相同时,也会存在非唯一 bean 的问题,这时候我们只需要在 @Autowired 上加个 @Qualifier("") // 在 "" 中指定 bean 的名称就可以了。


五、bean 的命名

5.1、五大注解存储 bean

  1. 前两位字目均为大写, bean 名称为类名
  2. 其他的为类名首字母小写
  3. Controller(value = "user")

5.2、@Bean 注解存储 bean

  1. bean 名称为方法名
  2. 通过 name 属性设置 @Bean(name = {"", ""})(可简写)

六、总结

通过本文的详细讲解,我们深入理解了 Spring 框架中 IoC 和 DI 的核心概念,并且通过多个实例,展示了如何在实际开发中应用这些概念。IoC(控制反转)通过将对象的创建和管理职责交给 Spring IoC 容器,使代码更加模块化和可维护。而 DI(依赖注入)则是实现 IoC 的一种具体方式,依赖注入将对象的依赖关系从容器中取出,大大降低了类与类之间的耦合度。

在实际应用中,IoC 和 DI 的结合不仅简化了对象的管理,还为系统架构的可扩展性和模块化提供了强有力的支持。Spring 提供了多种配置方式,如 XML 配置、注解配置和 Java 配置类,灵活地满足了不同开发需求。通过对比传统的对象创建与管理方式,我们可以清晰地看到,采用 IoC 和 DI 后,代码的灵活性和可测试性得到了极大的提升。


七、结语

Spring 框架中的 IoC 和 DI 为现代软件开发注入了新的活力。它们不仅使开发者能够专注于业务逻辑的实现,更通过简化依赖管理和提升代码的可维护性,奠定了系统架构的稳固基础。通过这篇文章,我们不仅深入了解了控制反转与依赖注入的概念和实现,还探索了它们在实际项目中的应用场景。

在未来的开发之路上,希望你能将 IoC 和 DI 的理念融入到日常的编码实践中,打造出更加灵活、高效的应用程序。记住,技术的学习不仅仅是知识的积累,更是思维的拓展和创新的探索。愿你在代码的世界中不断进步,成为技术领域的佼佼者,让每一行代码都闪耀智慧的光芒。勇敢追求技术梦想,未来属于不断创新的你!

IoC 与 DI : 简化 Spring 开发的秘密武器_依赖注入_10