IoC(控制反转) & DI(依赖注入)
学习目标:
- 理解 IoC 的基本理念,即控制权从应用程序代码转移到框架。
- 掌握依赖注入的多种方式:构造器注入、setter方法注入和字段注入。
一、IoC(控制反转)的基本理念
1.1 专业讲解
IoC 是一种设计原则,核心思想是将对象创建和依赖关系的管理工作从应用代码中移除,转交给外部容器(如Spring框架)来负责。
这意味着对象的实例化不再是通过代码直接new出来,而是由容器根据配置信息来创建和管理。
DI 是实现 IoC 的一种具体方式,它允许容器动态地将依赖对象注入到需要它们的组件中,从而减少组件之间的耦合。
1.2 通俗讲解
想象你在经营一家餐厅,原本每个厨师都需要自己去市场购买原料(自己创建依赖对象的实例),然后开始烹饪。这就像传统的编程方式,每个类自己创建并管理自己的依赖。
而采用IoC就像是有了一个中央厨房管理系统,这个系统知道每位厨师需要什么原料,它会提前准备好,厨师只需专注于烹饪(业务逻辑)。
DI就是这个管理系统将原料直接送到厨师手上的过程。
1.3 结合实际开发场景
假设我们正在开发一个订单处理系统,其中有一个 OrderService
类需要使用 PaymentService
来进行支付操作。
不使用 IoC 和 DI 时,我们可能在 OrderService
的构造方法或某个方法中直接new一个 PaymentService
实例。
但在Spring框架下,我们可以这样设计:
- 配置层面:告诉Spring容器
OrderService
需要一个PaymentService
的实例。 - 代码层面:在
OrderService
中不再直接创建PaymentService
实例,而是通过构造器或setter方法等待Spring容器在运行时自动注入所需的PaymentService
实例。
1.4 简易代码演示
传统方式(无IoC/DI):
public class OrderService {
private PaymentService paymentService = new PaymentService();
public void processOrder() {
// 使用paymentService进行支付操作
}
}
使用Spring IoC/DI:
1、首先定义接口和服务实现:
public interface PaymentService {
void pay();
}
@Service
public class DefaultPaymentService implements PaymentService {
@Override
public void pay() {
// 支付逻辑
}
}
2、接着在 OrderService
中声明对 PaymentService
的依赖,并通过注解或配置文件告知Spring容器:
@Service
public class OrderService {
private final PaymentService paymentService;
// Spring会自动找到匹配的PaymentService Bean并注入
// 这里采用的是构造器注入法,现在不用深入探究,到学习DI(依赖注入)部分会讲解注入方式
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void processOrder() {
paymentService.pay(); // 使用注入的paymentService进行支付操作
}
}
在这个过程中,Spring框架扮演了中央厨房管理系统的角色,它根据我们的配置自动管理对象的创建和依赖关系的注入,使得 OrderService
类更加专注于订单处理的业务逻辑,而不必关心 PaymentService
是如何被创建和获取的。
二、依赖注入的多种方式
依赖注入常见的三种方式:
- 构造器注入
- Setter方法注入
- 字段注入
2.1 构造器注入
2.1.1 专业讲解
通过类的构造函数来注入依赖。
当类实例化时,所有必需的依赖都必须通过构造函数传入。这保证了类在创建时就是完全初始化的状态,并且明确表示了它需要哪些依赖来正常工作。
这是最推荐的注入方式,因为它促进了 immutability(不变性)和易于理解的依赖关系。
2.1.2 通俗讲解
想象你组装一辆自行车,车架(主类)在出厂时就必须装配好车轮(依赖),没有车轮自行车无法出厂,这就像是构造器注入,没有依赖类就无法创建主类实例。
2.1.3 结合实际开发场景
假设开发一个博客系统,有一个 PostService
类需要依赖 PostRepository
来存取博客文章。
如果 PostService
必须有 PostRepository
才能正常工作,那么使用构造器注入,确保服务在创建时就具备了所有必要条件。
2.1.4 简易代码演示
@Service
public class PostService {
private final PostRepository repository;
@Autowired
public PostService(PostRepository repository) {
this.repository = repository;
}
// ...
}
2.1.5 优缺点
优点:
- 强制依赖性: 构造器注入确保了所有必需的依赖在对象创建时就被初始化,这有助于遵循“最少知识原则”,使得对象更加纯粹和易于理解。
- 不可变对象: 通过构造器注入的依赖通常作为final字段存储,有助于创建不可变对象,提高代码的安全性和并发性能。
- 清晰的依赖关系: 构造器参数列表清晰地展示了类的依赖项,使得代码更易于阅读和维护。
- 易于测试: 构造器注入的类更容易进行单元测试,因为可以通过构造器直接提供依赖的模拟或存根对象。
缺点:
- 构造器参数过多: 如果类依赖多个对象,构造器的参数列表可能变得冗长,影响代码的可读性。
- 不够灵活: 一旦对象被创建,构造器注入的依赖便不能更改,限制了运行时的灵活性。
2.2 Setter方法注入
2.2.1 专业讲解
使用setter方法来注入依赖。
这种方式允许在对象创建后再设置依赖,这种方式适用于可选的依赖或那些需要在对象生命周期中可变的依赖。
2.2.2 通俗讲解
同样以自行车为例,铃铛(可选依赖)可以在自行车买回家后,根据需要再安装上去。
这就是setter方法注入,依赖可以在对象创建之后随时添加或更改。
2.2.3 结合实际开发场景
如果 PostService
有个可选的日志记录组件 Logger
,不是必须的,但可以增强调试能力,此时可使用setter方法注入,在需要的时候添加这个功能。
2.2.4 简易代码演示
@Service
public class PostService {
private Logger logger;
@Autowired
public void setLogger(Logger logger) {
this.logger = logger;
}
// ...
}
2.2.5 优缺点
优点:
- 灵活性: setter方法允许在对象创建后动态地设置依赖,提供了更多的灵活性,特别是在依赖是可选或需要在运行时更改的情况下。
- 可选依赖: 对于非必需的依赖项,setter注入提供了更好的适应性,因为对象可以先被创建,然后按需设置这些依赖。
- 简化构造器: 避免了构造器参数列表过长的问题,使得构造器保持简洁。
缺点:
- 不明确的依赖关系: setter方法的调用顺序不固定,可能会导致依赖关系模糊,使得代码难以理解。
- 可变状态: 允许在运行时修改依赖,可能导致对象状态不稳定,增加了代码的复杂性和不确定性。
- 不保证完整性: 如果不小心忘记调用setter方法,对象可能处于未完全初始化的状态,尤其是在复杂的系统中容易出错。
2.3 字段注入
2.3.1 专业讲解
直接在类的成员变量上使用注解(如@Autowired、@Resource)来完成依赖的注入。
尽管这种方法最直接,但因为它减少了代码的清晰度和可测试性,通常不推荐作为主要的注入方式。
2.3.2 通俗讲解
想象你的书桌上有一个笔筒(类),你直接把笔(依赖)放在笔筒里,而不是通过某种仪式(方法调用)。字段注入就是这样直接,但可能导致笔筒(类)不知道笔(依赖)是怎么来的,降低了透明度。
2.3.3 结合实际开发场景
虽然直接,但不推荐作为首选,因为它减少了显式的依赖声明,可能影响代码的清晰度和测试。不过为演示,我们也可以考虑简单的配置属性注入作为例子。
2.3.4 简易代码演示
@Service
public class AppConfig {
@Value("${app.name}")
private String appName; // 直接注入配置属性到字段
}
// 或者
@Service
public class UserService {
@Resource
private UserMapper userMapper;
}
2.3.5、优缺点
优点:
- 简单快捷: 字段注入通过直接在类的成员变量上使用注解(如@Autowired、@Resource)来完成依赖的注入,这种方式最为直观且代码编写快速。
- 代码量少: 相比于构造器注入或setter方法注入,字段注入减少了需要编写的构造函数或setter方法的数量,使得类的定义看起来更为简洁。
缺点:
- 测试困难: 字段注入的类难以在没有Spring容器的情况下进行单元测试,因为依赖不能轻易地在测试中被替换或模拟(mock)。
- 耦合度高: 尽管字段注入能够实现依赖的注入,但它并未强制要求构造时就必须提供所有依赖,这可能导致类内部对外部依赖的强耦合,不利于代码的解耦和模块化。
- 不便于理解: 字段注入的依赖关系不如构造器注入明显,阅读代码的人需要查看字段注解来了解类的依赖,这降低了代码的可读性和维护性。
2.4 使用场景
2.4.1 构造器注入
1) 适合场景
当类的依赖是必需的,且希望保持对象的不可变性时,如领域模型、服务类等核心业务逻辑组件。
2) 模拟场景
场景说明: 在一个电子商务平台中,OrderService
负责处理订单相关的业务逻辑,包括创建订单、更新订单状态等操作。它必须依赖于 OrderRepository
来操作数据库中的订单数据,且这个依赖是不可或缺的,因为没有数据访问能力,OrderService
几乎无法执行任何操作。
使用理由: 构造器注入在这里非常合适,因为 OrderRepository
是 OrderService
的核心依赖,不可省略。通过构造器注入,确保了每次创建 OrderService
实例时,其数据访问能力已经就绪,且这个依赖是不可变的,增强了系统的稳定性和安全性。
2.4.2 setter注入
1) 适合场景
当依赖是可选的,或者需要在运行时根据条件动态调整时,如配置服务、某些辅助功能的启用与否。
2) 模拟场景
场景说明: 在同一个电商平台上,存在一个 NotificationService
用于发送订单状态变更的通知给用户,通知的方式可以通过邮件、短信或APP推送等多种渠道。其中, EmailSender
是一个可选的依赖,用于发送邮件通知,但不是每次订单状态变更都需要发送邮件,也可能根据用户偏好或系统配置决定是否启用邮件通知功能。
使用理由: setter注入适合这种场景,因为 EmailSender
不是一个硬性要求的依赖,服务启动时可能不需要立即设置。这样可以在应用启动后,根据配置或运行时的需要,动态地决定是否通过setter方法为 NotificationService
注入邮件发送功能,增加了系统的灵活性。
2.4.3 字段注入
1) 适合场景
原则上,字段注入因为其测试和维护上的劣势,通常不被推荐作为首选。
但如果是在快速原型开发或非常简单的场景下,为了快速验证功能,可能会暂时采用。
实际上字段注入在中小项目开发时使用最多。比如:若依框架。
2) 模拟场景
场景说明: 在快速搭建一个小型内部管理工具时,例如一个简单的日志记录工具 DebugLogPrinter
,它主要用于在开发阶段打印一些关键流程的日志信息,以帮助开发者调试。该工具只需要一个日志配置 LogConfig
来控制输出级别、格式等,而且在开发初期,追求快速迭代,不希望在构造或setter方法上花费太多时间。
使用理由: 在这样的快速原型开发或非常简单的工具类中,字段注入可以作为一种快速实现依赖注入的手段,减少代码量,快速验证功能。但需要注意,随着项目的复杂度上升,应该逐步过渡到构造器或setter注入,以保持代码的可维护性和测试性。字段注入在这里的使用是基于快速迭代和简化初期开发流程的考虑,而非长期的最佳实践。
2.5 面试题一:@Autowired & @Resource 用法区别
详细可以点击查看 @Autowired 或者 @Resource
@Autowired
和 @Resource
都是用于实现依赖注入的注解,但它们之间存在一些关键区别,主要体现在以下几个方面:
1、来源与默认行为
@Autowired: 它是Spring框架提供的注解,用于根据类型进行自动装配。如果一个类中有一个字段或方法使用了 @Autowired
,Spring容器会尝试找到一个匹配该类型(或其子类型)的Bean并注入。默认情况下,如果找不到匹配的Bean,Spring会抛出异常。可以通过 required=false
属性改变这一行为,允许注入为null。
@Resource: 它来自Java EE规范(JSR-250),虽然也被Spring支持,但它的默认行为是按照名称来查找Bean。如果 @Resource
注解没有显式指定name属性,那么它会默认使用字段名或方法名作为Bean的名称去查找。如果找不到匹配名称的Bean,根据具体的容器(如Tomcat)行为可能会有所不同,有的容器可能会尝试按类型匹配,但这种行为不是规范规定的,主要还是依赖于具体实现。
2、注解属性与灵活性
@Autowired
提供了一些额外的灵活性,比如可以用来注解构造函数、setter方法、字段以及具有任意名称和多个参数的方法,同时可以通过 @Qualifier
注解进一步精确控制基于名称的匹配。此外 @Autowired
还支持泛型依赖的解析。
@Resource
主要用于字段和方法,且更倾向于使用名称进行匹配。虽然也可以通过name属性指定Bean名称,但它缺乏 @Qualifier
那样的灵活性来进一步限定类型匹配。
3、默认行为差异
当没有明确指定匹配规则时,@Autowired
默认按类型匹配,而 @Resource
默认尝试按名称匹配(名称匹配失败时的行为依赖于容器)。
4、实际选择
如果你的项目主要是Spring生态,且依赖注入主要基于类型匹配,@Autowired
可能是更自然的选择,特别是当你需要利用Spring的高级特性(如 @Qualifier
、构造器注入等)时。
如果你的代码需要遵循Java EE规范,或者你希望依赖注入优先考虑Bean的名称,那么 @Resource
可能更合适。
啦啦啦!😀