IOC(Inversion of Control,控制反转)是面向对象编程中的一种设计思想,将程序中创建对象的控制权交给外部环境(Spring 框架、IoC 容器 )来管理,以便降低计算机代码之间的耦合度。
IoC 容器是用来实现 IoC 的载体,负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。在传统的编程模式中,程序员负责创建和管理对象及其依赖关系,而在控制反转的设计中,这些控制权被转移到外部容器或框架,程序员只需关注业务逻辑,具体的对象创建和管理由容器负责。
例如,现有类 A 依赖于类 B
传统的开发方式 : 类A会直接在其代码中通过 new
关键字创建类B的实例。
// 传统方式创建对象及其依赖对象
public class A{
private B dependency = new B();
public void doSomething() {
dependency.performTask();
}
}
使用 IoC 思想的开发方式 : 通过IoC容器(例如Spring容器),我们可以直接从容器中直接获取类 B 的 Bean( ApplicationContext.getBean(B.class)
), 将类 A 的依赖(类 B )从外部注入,而不是在类A内部直接创建。
// 使用IOC容器后的对象声明方式
public class IoCApplication {
private B dependency;
// 依赖注入(方法注入)
public IoCApplication(B dependency) {
this.dependency = dependency;
}
public void doSomething() {
dependency.performTask();
}
}
控制反转的实现方式主要有以下几种:
依赖注入(Dependency Injection,DI):通过构造函数、属性或方法参数传入依赖对象,使得对象的依赖关系在运行时由外部容器管理。
依赖查找(Dependency Lookup):组件需要自己从容器或上下文中查找和获取依赖对象。
Spring 框架的核心模块 spring-core 主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块。
spring-core:Spring 框架基本的核心工具类。
spring-beans:提供对 bean 的创建、配置和管理等功能的支持。
spring-context:提供对国际化、事件传播、资源加载等功能的支持。
spring-expression:提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
如果让我们重新设计一个 Spring IoC ,我们需要考虑的内容主要有:
Bean 的生命周期管理: 管理对象的创建、初始化、销毁等生命周期。
Bean 的作用域: 支持多种 Bean 作用域,比如单例、原型、会话、请求等,可以考虑使用 Map 来存储不同作用域的 Bean 实例。
配置管理模块: 管理应用配置(包括 XML、注解、Java 配置类等)
依赖注入模块: 自动注入需要的依赖,支持构造器注入、属性注入、方法注入等。
依赖注入(DI)是一个过程,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后设置的属性来定义其依赖项(即它们使用的其他对象)。然后,IoC容器在创建bean时注入这些依赖项。这个过程基本上是bean本身的逆过程(因此称为控制反转),通过使用类的直接构造或服务定位器模式来控制其依赖关系的实例化或位置。
在 Spring 框架里面,依赖注入的最大亮点是所有的 Bean 对 Spring 容器的存在没有意识。我们甚至可以将 Spring 容器替换成其他的容器。
在依赖注入的实现方式里面,最常用的是构造函数注入和 setter 方法注入。
构造函数注入:依赖对象通过构造函数传入。
@Service
public class Service {
private final Repository repository;
// 构造器底层会使用注解 @Autowired ,可以隐式使用
public Service(Repository repository) {
this.repository = repository;
}
}
setter 方法注入:依赖对象通过 setter 方法注入。
public class Service {
private Repository repository;
// 可以隐式使用 @Autowired
public void setRepository(Repository repository) {
this.repository = repository;
}
}
字段注入:使用@Autowired
注解直接在字段上注入依赖。这种方式通常用于小型项目或者在测试时注入模拟对象。不建议使用字段注入。
@Service
public class Service {
@Autowired
private Repository repository;
}
方法注入:当需要在依赖注入的同时执行复杂的初始化逻辑时,可以使用普通的方法注入。
@Service
public class Service {
private Repository repository;
@Autowired
public void initialize(Repository repository) {
this.repository = repository;
// 可能还有其他初始化逻辑
}
}
具体细节查看 Dependency Injection :: Spring Framework
字段注入 | setter 方法注入 | 构造函数注入 | |
---|---|---|---|
优点 | - 代码简洁,无需编写构造函数或 setter 方法 | - 允许在实例对象创建后再注入依赖对象,具有较高的灵活性 - 适合可选依赖的场景 |
- 保证注入的 Bean 不为空 - 项目启动时会直接对循环依赖报错,从而避免运行时才发现。因为构造函数注入导致的循环依赖没有解决方案。 |
缺点 | - 注入的 Bean 可能出现空指针异常。Field 注入允许构建对象实例时依赖的对象为空 | - 可能出现循环依赖问题。三级缓存机制可以解决 setter 注入部分情况下的循环依赖,但不能完全解决。 | - 不适用于可选依赖,所有依赖必须在对象创建时提供 |
同时,只有使用构造函数注入才能注入 final 变量, 字段注入和 setter 方法注入都不支持。
Spring 团队通常提倡 构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖关系不为空。此外,constructor-injected组件总是以完全初始化的状态返回给客户端(调用)代码。
在使用 Bean 注入时,需要注意以下事项:
使用构造函数注入的建议有:
@RequiredArgsConstructor
,避免手写构造方法。@RequiredArgsConstructor
注解是针对标有 @NonNull
注解的变量和 final 变量进行参数的构造方法。相关代码为:
@Component
public class Repository{
// ...
}
@RequiredArgsConstructor
@Service
public class Service {
private final Repository repository;
// ...
}
谨慎使用字段注入,因为它可能导致不易测试的代码。构造函数或 Setter 方法注入通常更容易测试。
同时,字段注入无法避免空指针异常。
@Resource 会增加额外代码, SpringBoot3 及以上已经废弃了 @Resource 注解。
如果容器中有多个与 @Autowired
注解字段类型相匹配的 Bean,Spring 将无法确定应注入哪个 Bean,这可能会导致注入冲突或异常。在 SpringBoot3 以下版本中,使用该注解会提示波浪线。例如
@Service
public class MyService {
@Autowired
private MyRepository myRepository; // 如果有多个 MyRepository 的实现,Spring 会无法决定注入哪个
}
java - Spring Boot Bean注入方式详解与实践 - 个人文章 - SegmentFault 思否
Introduction to the Spring IoC Container and Beans :: Spring Framework
Dependency Injection :: Spring Framework
Spring IOC深度解析:IOC容器的原理与高级特性详解_ioc容器的原理,深入理解如何通过反射和继承机制实例化类-CSDN博客
为什么采用构造器注入? (yuque.com)
Spring常见面试题总结 | JavaGuide