Spring IoC&DI(上)

前言:在现代的Java开发中,IoC(Inversion of Control)和DI(Dependency Injection)是两个非常重要的概念。它们的引入使得代码更加模块化、可测试性更高、可维护性更强。本文将深入探讨IoC和DI的概念及用法。

1. 初始IoC&DI

1.1 Spring是什么?

通俗的说,Spring就是包含了众多⼯具⽅法的IoC容器。
🎧🎧想象一下,你在做一道菜,需要使用各种各样的厨具和调料。Spring如同一个厨房,里面摆放着各种各样的厨具(工具)和调料(方法)。你可以从这个厨房里自由地拿取你需要的东西,进行烹饪。所以,IoC容器就等同于整个厨房,它负责管理和提供这些工具和方法。你不需要自己去买厨具或者调料,也不需要自己去管理它们的摆放和使用方式。只需要告诉IoC容器你需要什么,它就会帮你准备好,让你专心做菜即可。因此,Spring框架作为一个IoC容器,为开发者提供了各种各样的工具和方法,使得开发变得更加便捷和高效。

1.2 什么是IoC?

  • IoC(Inversion of Control),即控制反转,是一种设计模式(思想),它将应用程序控制权的流向从应用程序代码本身转移到外部容器或框架中。简单来说,IoC将创建对象和管理对象之间的依赖关系的责任交给了容器,而不是由代码自己管理。

1.3 传统程序开发流程

假设我们需要造一辆汽车,我们会有如下设计思路:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽⻋(Car)。这里就出现了⼀个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

依赖
依赖
依赖
汽车Car
车身Framework
底盘Bottom
轮胎Tire

1.4 IoC程序开发

此时一旦出现需求的变更,代码的编写就会变得十分繁琐,这与我们软件设计的原则—高内聚,低耦合背道而驰,这时候我们不妨反向思考一波:

依赖
依赖
依赖
轮胎Tire
底盘Bottom
车身Framework
汽车Car

这就等同于我们打造一辆完整的汽车时,如果所有的配件都是由我们自己制造的,那么当客户需求发生变化时,比如说轮胎的尺寸不再是原来的尺寸了,我们就需要自己动手来修改。但如果我们把轮胎的制造外包给其他厂家,即使轮胎的尺寸发生了变化,我们只需要向代工厂下订单就可以了,不需要自己亲自去制造。这样,我们可以更专注于汽车的设计和装配工作,而不用担心每一个配件的变动会给我们带来额外的麻烦和成本。

1.5 IoC的优势

通过上述两种开发方式,我们发现了一个规律:在传统的程序实现中,类的创建顺序是反着的。举个例子,假设我们有一个汽车类(Car),它负责控制并创建了一个框架类(Framework),框架类又创建并控制了底盘类(Bottom),以此类推,依次往下。
而在改进后的实现中,控制权发生了反转。不再是由使用方对象创建并控制依赖对象,而是将依赖对象注入到当前对象中。这样一来,依赖对象的控制权不再由当前类控制了。
这样做的好处是,即使依赖类发生了任何改变,当前类都不会受到影响。这就是典型的控制反转,也就是IoC的实现思想。

因此,我认为IoC拥有以下三点优势:📌

  1. 解耦合:对象之间的依赖关系由外部容器管理,减少了对象之间的耦合,提高了代码的灵活性和可维护性。
  2. 资源集中管理:IoC容器会帮我们管理⼀些资源(对象等),我们需要使⽤时,只需要从IoC容器中去取就👌
  3. 可扩展性:由于依赖关系被解耦,因此可以更轻松地对代码进行扩展和修改,而不会影响到其他部分的代码。

2 什么是DI?

  • DI(Dependency Injection),即依赖注入(容器在运行期间,动态的为应用程序提供运行时所依赖的资源),是IoC的一种具体(实现)方式。通过DI,容器负责将所需的依赖注入到相应的对象中,从而实现了对象之间的解耦。这种方式使得代码更加灵活、可测试和可维护。

3. IoC&DI如何使用?

前面我们谈到Spring是一个IoC(控制反转)容器,作为一个容器,那便一定有的功能,说白了,IoC就是Bean的存储👊👊

此处Spring管理的基本上是对象,而这些对象我们通常称为Bean。我们把这些对象交由、给Spring管理从而实现控制权的反转,由Spring来负责对象的创建/销毁;程序只需要告诉Spring,哪些需要存?以及如何从Spring中取出对象?

⽽Spring框架为了更好的服务web应用程序,提供了更丰富的注解,通常会用到以下两类注解:

  • 类注解:
    @Controller:用于标识一个类是控制器组件,通常用于处理HTTP请求和响应。
    @Service:用于标识一个类是服务层组件,通常用于业务逻辑的处理。
    @Repository:用于标识一个类是数据访问层组件,通常用于数据库操作。
    @Component:用于标识一个类是组件,Spring会自动扫描并将其实例化为Bean。
    @Configuration:用于标识一个类是配置类,通常用于配置Bean的创建和依赖关系。
  • 方法注解:
    @Bean:用于在配置类中声明方法,这些方法返回一个对象,Spring 容器会将这个对象作为一个 Bean 进行管理。

3.1 @Controller(控制器存储)

使用@Controller存储bean:

接下来我们试着从Spring容器中获取该对象

运行结果如下:

而将@Controller注解删除则出现如下异常:(找不到类型是com.example.demo.controller.UserController的bean的定义)

获取bean对象的其他方式:

从该图我们不难看出,getBean()方法提供了三种获取bean对象的方式

接下来我们不妨试验一下,并运行…


🆘🆘**注意:bean的name为类名的小驼峰样式(即 以小写字母开头,然后使用驼峰式
大小写),且如果类名的前两位均为大写时,bean的name即为类名。**否则便会出现如下错误:(No bean named 'aController' aavailable)

3.2 其他四大类注解

由于这四大注解的使用同@Controller类似,在此就不过多赘述了,它们都是存储bean、实现组件化、自动化和依赖注入的时使用的;只是在语义和用途上有所不同,用于标识不同层次的组件,以便Spring框架能够自动扫描并管理它们。

  • @Controller(控制器存储)
  • @Service(服务存储)
  • @Repository(仓库存储)
  • @Component(组件存储)
  • @Configuration(配置存储)

3.3 那么为什么要引入这些类注解呢❓❓

其实主要是为了让我们看到类注解后,直接就能知道这个类的用途。这与应用分层的理念是契合的。

  • @Controller:控制层,接收请求,对请求进行处理,并进行响应.
  • @Service:业务逻辑层,处理具体的业务逻辑.
  • @Repository:数据访问层(持久层)负责数据访问操作.
  • @Configuration:配置层,处理项目中的⼀些配置信息.
    相应的调用流程如下:

3.3.1 类注解之间的关系


如图,我们在查看了@Controller/@Service/@Repository/@Configuration注解的源码发现:这些注解的源码中都存在着一个子注解@Compnetnt,意味着这些注解是@Compnent的子类。可以这么理解:@Compnent作为一个元注解,可以注解其他类注解,就是通用的意思,这些其他类注解被称之为@Compnent衍生注解。然而,这些衍生注解往往被用于更具体的用例,也算是“术业有专攻”吧!

3.4 方法注解@Bean

前面谈到的类注解都是添加在类上的,但有两种特殊的情况:

  1. 使用外部jar包里的类时,是无法添加类注解的
  2. 当一个类中需要多个对象,定义多个数据源时也是没有办法添加类注解
    那么,这些场景下我们就需要使用方法注解**@Bean**

那不妨试试🙏🙏🙏🙏

奈何压根就获取不到,直接报错了😂

3.4.1方法注解必须配合类注解使用

在Spring框架的设计中,方法注解 @Bean要配合类注解才能告诉Spring,类中有需要帮忙管理的对象并扫描下面的方法,将对象正常的存储到Spring容器中。
代码与运行结果如图:

3.4.2 定义多个对象

话不多说,直接实操一波

此时同样使用getBean()方法获取bean对象,但此时又报错了😅😅

大致意思是:没有唯一的bean的定义,userInfo预期是单个,却发现了两个:user1,user2

从这段报错信息看出,bean的名称应该就是它的方法名,我们不妨根据名称来获取bean对象

@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context =
                SpringApplication.run(SpringIocDemoApplication.class, args);
        //根据bean名称, 从Spring上下⽂中获取对象
        UserInfo userInfo1 = (userInfo) context.getBean("user1");
        UserInfo userInfo2 = (userInfo) context.getBean("user2");
        System.out.println(userInfo1);
        System.out.println(userInfo2);
    }
}

运行结果:

3.4.3 对Bean重命名

点进@Bean注解源码中我们发现:
@AliasFor注解用于创建属性之间的别名,这在自定义注解时特别有用,因为它允许属性之间共享相同的值;value和name这两个属性都被标记为彼此的别名。所以我们可以通过这个属性直接指定bean的名称。

示例如下:

📌注意:
1.name = 可以省略,形如@Bean({"u1","user1"})
2. 只有⼀个名称时,{}也可以省略,形如:@Bean("u1")

3.4.4 @Bean传递参数

如果我们不把UserInfo中的name写死,而是通过传参的方式去setName,需要怎么做呢?

这里我就不逐一演示了,大家只要知道用法及注意事项即可;大致可以总结成两句话:

  • 如果需要的Bean的类型对应的对象只有一个时,那就直接赋值
  • 如果存在多个时,通过名称去匹配,而非代码先后顺序

3.5 扫描路径

前面使用注解声明的bean,一定会生效吗❓
答案是:不一定,因为bean要想生效,还必须被Spring扫描
接下来我们试试将项目启动类挪到controller包中,测试下bean对象是否生效:

结果是:真的报错了😭😭😭😭😭😭

那么为什么会报错呢?
Spring Boot有一大特点,约定大于配置,默认扫描路径是启动类所在目录及其子孙目录,所以我们此时需要引入一个新的注解@ComponentScan来指定扫描路径。

进入该注解的源码,我们发现有一个根目录的属性,可以通过手动设置该目录从而指定扫描路径,添加后程序已正常运行。

那这个时候或许有人会产生疑问了,为啥不移动启动类时程序能够正常运行,或许在@SpringBootApplication中能获取到答案

原来@SpringBootApplication注解本身便包含了@ComponentScan,表示默认扫描当前类目录及其子目录。但一般情况下,启动类默认放在项目最外层。

未完待续…(下篇将对DI进行详细介绍)

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值