SpringBoot 中的 IoC & DI 入门
- IoC:控制反转
- 站内文章AOP:面向切面编程
基本概念
控制反转 Inversion of Control
控制反转(Inversion of Control,IoC) 即控制反转/反转控制。它是一种设计思想,而不是一个技术实现。它描述的是 Java 开发领域对象的创建以及管理的问题。这种设计思想可以用来指导框架层面的设计。
- 控制 :指的是对象创建(实例化、管理)的权力,对程序执行流程的控制。
- 反转 :控制权交给外部环境(IoC 容器)。在没有反转之前,程序员自己控制整个程序的执行。在使用某个控制反转框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员「反转」到了框架。
依赖注入 Dependency Injection
IoC 最常见以及最合理的实现方式叫做 依赖注入(Dependency Injection,DI)。它是一种具体的编码技巧:不通过 new
的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
下面通过简单例子展示 DI:手机类 Phone
存在内部成员 SIMCard
卡。插入 SIMCard
的手机才能使用,调用 SIMCard
类中的方法。
下面展示 Phone
非依赖注入实现的过程:
1 | // 非依赖注入实现方式 |
依赖注入的实现过程:
1 | // 依赖注入实现方式 |
依赖注入框架 DI Framework
在上面的手机和 SIM 的例子中,依赖注入的实现过程虽然不存在硬编码在 Phone
内部创建 SIMCard
类的方式,但是 SIMCard
类的创建、组装(注入)Phone
的工作仅仅是被移动到了更加上层的代码而已,还是需要我们程序员自己来实现。
在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。这个框架就是「依赖注入框架」。
现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。
Spring 是依赖注入框架,而它自称是控制反转容器(Inversion Of Control Container)。这两种说法都没错。控制反转容器这种表述是一种非常宽泛的描述,DI 依赖注入框架的表述更具体、更有针对性。控制反转的方式有很多,除了依赖注入,还有模板模式等,而 Spring 框架的控制反转主要是通过依赖注入来实现的。
依赖反转原则 Dependency Inversion Principle
详见:站内文章设计原则
Spring 中 IoC/DI 的使用
Bean 对象:IoC 容器中创建、管理的对象,称之为 bean。
基础使用:
@Component
:将当前类(Service 层、Dao 层)交给 IoC 容器管理,成为 IoC 容器中的 bean。实现控制反转。@Autowired
:(为 Controller 和 Service 注入)运行时,IoC 容器会提供该类型的 bean 对象,并赋值给该变量。实现依赖注入。
所以说,切换业务实现类时,直接把需要用的实现类上加上
@Component
就行。
SpringBoot 中 Bean 的声明:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明 bean 的基础注解 | 不属于以下三类时,用此注解(比如一些工具类) |
@Controller | @Component 的衍生注解 | 标注在控制器类上 |
@Service | 标注在业务类上 | |
@Repository | 标注在数据访问类上(由于与 mybatis 整合,用的少) |
声明 bean 的时候,可以通过 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写。
使用以上四个注解都可以声明 bean,但是在 Springboot 集成 web 开发中,声明控制器 bean 只能用 @Controller
。
@RestController
中包含@Controller
注解。
Bean 组件扫描:
- 前面声明 bean 的四大注解,要想生效,还需要被组件扫描注解
@Componentscan
扫描。@ComponentScan
注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication
中,默认扫描的范围是启动类所在包及其子包。 - 【不推荐】我们可以在启动类上增加
@ComponentScan
注解,手动设置扫描包的范围。注意检查有没有覆盖掉默认的薮猫范围,可以自己手动添加上。
Bean 注入:
@Autowired
注解,默认是按照类型进行。也就是在 IoC 容器中找这个类型的 Bean 对象然后再注入。- 如果存在多个相同类型的 bean,将会报错,可以使用以下方案解决:
@Primary
。在 Bean 上面再加上这个注解,可以让该 Bean 优先生效。@Autowired
+@Qualifier("<beanName>")
。使该名字的 Bean 生效。@Resource(name="<beanName>")
。使该名字的 Bean 生效。
@Resource
与 @Autowired
区别:@Autowired
是 Spring 框架提供的注解,而@Resource
是 JDK 提供的注解,@Autowired
默认是按照类型注入,而@Resource
默认是按照名称注入。
示例工程
一个工程目录示例:
示例 Mapper:
1 |
|
示例 Service 接口类:
1 | public interface DeptService{ |
示例 Service 的实现类:
1 |
|
示例 Controller 的写法:
1 | // 使用日志 |
Bean 管理
获取 Bean
1 | // 拿到 IoC 容器的方法 |
默认情况下,Spring 项目启动时,会把 bean 都创建好放在 IoC 容器中 [1],如果想要主动获取这些 bean,可以通过如下方式使用 ApplicationContext
的方法:
- 根据 name 获取 bean:
Object getBean(String name)
- 根据类型获取 bean:
<T> T getBean(Class<T> requiredType)
- 根据 name 获取 bean(带类型转换):
<T> T getBean(String name, Class<T> requiredType)
示例:
1 |
|
输出结果发现三个 bean 对象都是同一个对象,说明此时 bean 是单例的。
Bean 的作用域
Spring 支持五种作用域,后三种在 web 环境才生效:
作用域 | 说明 |
---|---|
singleton | 容器内同名称的 bean 只有一个实例(单例)(默认) |
prototype | 每次使用该 bean 时会创建新的实例(非单例) |
request | 每个请求范围内会创建新的实例(web 环境中,了解即可) |
session | 每个会话范围内会创建新的实例(web 环境中,了解即可) |
application | 每个应用范围内会创建新的实例(web 环境中,了解即可) |
可以通过 @Scope
注解来进行配置作用域:@Scope("prototype")
。
默认 singleton
的 bean,在容器启动时被创建,可以使用 @Lazy
注解来延迟初始化(延迟到第一次使用时),prototype
的 bean,每一次使用该 bean 的时候都会创建一个新的实例。
实际开发当中,绝大部分的 Bean 是单例的,也就是说绝大部分 Bean 不需要配置 scope 属性。
第三方 Bean
如果要管理的 bean 对象来自于第三方(不是自定义的),是无法用 @Component
及衍生注解声明 bean 的(第三方某些类的源码定义中我们无法改动),就需要用到 @Bean
注解。若要管理的第三方 bean 对象,建议对这些 bean 进行集中分类配置,可以通过 @Configuration
注解声明一个配置类。
@Configuration
底层也是@Component
。
1 | // 方法一:【不推荐】定义在启动类中 |
使用 Bean 时直接注入即可:
1 |
|
如果第三方 bean 需要依赖其它 bean 对象,直接在 bean 定义方法中设置形参即可,容器会根据类型自动装配。
1 |
|
@Component
及衍生注解与 @Bean
注解使用场景?
- 项目中自定义的,使用
@Component
及其衍生注解 - 项目中引入第三方的,使用
@Bean
注解
后记
下面你可以立即移步这篇文章加深上面所学知识的印象:站内文章SpringBoot 的原理以及写一个自定义 Starter。
本文参考
- Day05-09. 分层解耦-三层架构_哔哩哔哩_bilibili
- IoC & AOP详解(快速搞懂) | JavaGuide
- 设计模式之美 - 王争 - 极客时间专栏
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
这还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的 bean 而言。 ↩︎