依赖注入 DI
对依赖注入(Dependency Injection, DI)的理解
依赖注入(DI)是 控制反转(IoC)的核心实现方式,也是面向对象编程中解耦的重要设计模式。其核心思想是:让类的依赖(即它需要的外部资源,如其他类实例、配置信息等)由外部容器或框架负责创建和 “注入”,而非类自身主动创建—— 简单说就是 “谁需要依赖,谁就被动接收,而非自己找”。
一、先搞懂:为什么需要依赖注入?(解决什么问题)
没有 DI 时,类通常会 “主动创建依赖”,导致代码强耦合、难测试、难维护。
反例(无 DI,强耦合)
// 业务类:需要依赖 UserDao 操作数据库
public class UserService {
// 主动创建依赖:UserService 与 UserDao 强耦合
private UserDao userDao = new MySQLUserDao(); // 硬编码依赖实现
public void queryUser() {
userDao.query();
}
}
// 依赖的实现类(MySQL 版本)
class MySQLUserDao {
public void query() {
System.out.println("MySQL 查询用户");
}
}
问题所在:
- 强耦合:
UserService直接依赖MySQLUserDao的具体实现,若后续要切换为OracleUserDao,必须修改UserService的代码(违反 “开闭原则”); - 难测试:
UserDao直接绑定数据库,无法在单元测试中用 “模拟对象(Mock)” 替代,只能连接真实数据库测试; - 责任混乱:
UserService的核心责任是 “业务逻辑”,却还要负责 “创建依赖实例”,违背 “单一职责原则”。
正例(有 DI,解耦)
// 1. 定义依赖的接口(抽象依赖,而非具体实现)
interface UserDao {
void query();
}
// 2. 具体实现类(MySQL/Oracle 均可)
class MySQLUserDao implements UserDao {
@Override
public void query() {
System.out.println("MySQL 查询用户");
}
}
class OracleUserDao implements UserDao {
@Override
public void query() {
System.out.println("Oracle 查询用户");
}
}
// 3. 业务类:不主动创建依赖,而是通过构造器“接收”依赖(注入)
public class UserService {
private UserDao userDao;
// 构造器注入:依赖由外部传入,UserService 只关心依赖的接口,不关心具体实现
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void queryUser() {
userDao.query();
}
}
// 4. 外部容器/代码负责创建依赖并注入
public class Main {
public static void main(String[] args) {
// 第一步:创建依赖实例(可配置化,无需修改 UserService)
UserDao userDao = new MySQLUserDao(); // 切换为 OracleUserDao 只需改这里
// 第二步:注入依赖到业务类
UserService userService = new UserService(userDao);
// 调用业务方法
userService.queryUser();
}
}
改进效果:
- 解耦合:
UserService依赖UserDao接口(抽象),而非具体实现,切换数据库只需修改 “依赖创建” 的代码,业务类无需改动; - 易测试:单元测试时可传入
MockUserDao(模拟对象),无需连接真实数据库; - 责任清晰:
UserService专注业务逻辑,依赖的创建由外部负责。
二、依赖注入的核心原则
- 依赖抽象,不依赖具体实现(面向接口编程):类只声明对 “接口 / 抽象类” 的依赖,避免绑定具体实现类;
- 依赖由外部注入:类自身不通过
new、static等方式创建依赖,仅提供 “接收依赖” 的入口(如构造器、setter 方法); - 控制反转:依赖的创建、生命周期管理(如销毁)由外部容器(如 Spring 容器)控制,而非类自身控制 —— 这是 DI 的本质。
三、依赖注入的常见实现方式(Java 中)
实际开发中,DI 通常由框架(如 Spring、Guice)自动实现,核心注入方式有 3 种:
1. 构造器注入(推荐)
通过类的构造器参数接收依赖,是最常用、最安全的方式(确保类实例创建时依赖已完全初始化)。
public class UserService {
private final UserDao userDao; // final 保证依赖不可变
// 构造器注入(Spring 中可通过 @Autowired 或无注解(Spring 4.3+)自动注入)
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
优点:
- 强制依赖必须传入,避免类实例化后依赖为
null; - 依赖用
final修饰,线程安全。
2. Setter 方法注入
通过 setXxx() 方法接收依赖,适合 “可选依赖”(即不注入也不影响类的基本功能)。
public class UserService {
private UserDao userDao;
// Setter 注入(Spring 中用 @Autowired 标注)
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
优点:
- 灵活性高,可在类实例化后动态修改依赖;
缺点:
- 可能导致类实例化后依赖未注入(需手动调用 setter),存在
NullPointerException风险。
3. 字段注入(慎用)
直接在类的字段上标注注入注解(如 Spring 的 @Autowired),框架通过反射直接给字段赋值,无需构造器或 setter。
public class UserService {
// 字段注入(Spring 通过反射注入)
@Autowired
private UserDao userDao;
}
优点:
- 代码简洁,无需编写构造器 /setter;
缺点:
- 字段通常为
private,依赖反射实现,可读性差、调试困难; - 类实例化后依赖可能为
null,且无法用final修饰; - 不利于单元测试(需通过反射设置字段值)。
四、依赖注入的核心价值(为什么广泛使用)
- 解耦:降低类与类之间的耦合度,便于代码重构、扩展(如切换依赖实现);
- 提高可测试性:依赖可替换为模拟对象,支持单元测试隔离;
- 简化开发:依赖的创建、管理由框架自动完成,开发者无需关注底层细节;
- 提高代码复用性:依赖组件可被多个类共享(如 Spring 容器中的单例 Bean);
- 便于维护:依赖的配置集中管理(如 Spring 的
applicationContext.xml或注解),修改时无需改动业务代码。
五、依赖注入与 IoC 的关系
很多人会混淆 DI 和 IoC,核心关系是:IoC 是设计思想,DI 是 IoC 的具体实现方式。
- 控制反转(IoC):描述 “依赖的控制权限从类自身转移到外部容器” 的思想;
- 依赖注入(DI):是实现 IoC 的最核心、最常用的手段 —— 通过 “注入” 依赖,实现 “控制反转”。
简单说:IoC 是 “目的”,DI 是 “手段”。
六、实际应用场景
DI 广泛用于中大型项目,尤其是需要解耦、高可维护性的场景:
- 框架开发:Spring 容器的核心就是 DI(Bean 的依赖自动注入);
- 业务系统:服务层(Service)依赖数据访问层(Dao)、控制器(Controller)依赖服务层(Service)的场景;
- 第三方组件集成:如集成 Redis、MQ 等组件时,通过 DI 注入
RedisTemplate、RabbitTemplate等依赖,无需手动创建。
总结
依赖注入的本质是 “解耦”—— 通过 “外部注入依赖” 替代 “类自身创建依赖”,让类更专注于核心职责,同时提升代码的可测试性、可维护性和扩展性。其核心是 “面向接口编程” 和 “控制反转”,实际开发中通过 Spring 等框架可快速实现,无需关注底层依赖的创建和管理。