微恐连续剧
67.38M · 2026-02-05
作为Java开发中不可或缺的核心设计模式,依赖注入(Dependency Injection,简称DI)早已融入Spring、Spring Boot等主流框架的血脉之中。它不仅彻底解决了传统开发中“高耦合、难测试、难维护”的痛点,更奠定了企业级应用“松耦合、高内聚”的架构基础。很多开发者每天都在使用@Autowired、@Inject等注解,但往往只知其然,不知其所以然——DI的核心本质是什么?三种注入方式有何区别?Spring是如何实现依赖注入的?实际开发中又该如何规避常见陷阱?本文将从基础到进阶,层层拆解,帮你真正吃透Java依赖注入。
在讲解DI之前,我们先明确两个核心概念:依赖与控制反转(IoC) ——DI是IoC的具体实现方式,理解IoC才能真正理解DI。
在Java开发中,依赖指的是两个类之间的关联关系:如果类A需要调用类B的方法来完成业务逻辑,那么我们就说“类A依赖于类B”。
举个最直观的反例(传统开发模式):
// 服务层:用户服务,依赖于用户DAO(数据访问层)
public class UserService {
// 直接在类内部创建依赖对象——高耦合的根源
private UserDao userDao = new UserDaoImpl();
// 业务方法,依赖UserDao完成数据操作
public User getUserById(Long id) {
return userDao.selectById(id);
}
}
这种写法的问题非常明显:
而依赖注入的核心目的,就是解除这种强耦合:将“创建依赖对象”的控制权从依赖类(UserService)中转移出去,由第三方(比如Spring容器)统一管理,再将依赖对象“注入”到依赖类中。
依赖注入(DI):一个类所依赖的对象,不由该类自身创建,而是由外部容器创建并注入到该类中,以此实现类与类之间的解耦。
简单来说,就是“谁依赖谁,谁注入谁,注入什么”:
改造上面的代码(DI模式):
// 1. 定义Dao接口
public interface UserDao {
User selectById(Long id);
}
// 2. Dao实现类(多个实现可灵活替换)
public class UserDaoImpl implements UserDao {
@Override
public User selectById(Long id) {
// 模拟数据库查询
return new User(id, "张三");
}
}
// 3. 服务层:不再创建依赖对象,而是等待外部注入
public class UserService {
// 依赖对象(由外部注入)
private UserDao userDao;
// 方式1:构造器注入(推荐)
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 业务方法
public User getUserById(Long id) {
return userDao.selectById(id);
}
}
// 4. 外部容器(模拟Spring):创建依赖对象,注入到服务类中
public class SpringContainer {
public static void main(String[] args) {
// 1. 创建被依赖对象
UserDao userDao = new UserDaoImpl();
// 2. 创建依赖方,并注入被依赖对象
UserService userService = new UserService(userDao);
// 3. 调用业务方法
User user = userService.getUserById(1L);
System.out.println(user);
}
}
改造后,UserService不再依赖于具体的UserDaoImpl,只依赖于UserDao接口——如果需要替换Dao实现,只需修改容器中的创建逻辑,无需改动UserService源码,彻底实现了解耦。这就是依赖注入的核心价值。
在Java中,依赖注入主要有三种实现方式,各有优劣,实际开发中需根据场景选择。其中,构造器注入是Spring官方推荐的方式,字段注入则因存在隐患,被不推荐使用(但仍广泛被误用)。
通过类的构造方法,将被依赖对象注入到依赖类中。这是最安全、最规范的注入方式。
Spring中的使用示例(注解方式):
@Service // 标记为Spring管理的Bean
public class UserService {
// 声明为final,保证不可变
private final UserDao userDao;
// 构造器注入:Spring会自动找到UserDao类型的Bean注入
@Autowired // Spring 4.3+后,单个构造器可省略@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 业务方法
public User getUserById(Long id) {
return userDao.selectById(id);
}
}
通过类的setter方法,将被依赖对象注入到依赖类中。适合“可选依赖”(即依赖对象可有可无,不影响类的核心功能)。
Spring中的使用示例:
@Service
public class UserService {
private UserDao userDao;
// Setter方法注入
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
// 可选:提供无参构造器
public UserService() {}
public User getUserById(Long id) {
// 需注意:如果未注入userDao,会报NullPointerException
return userDao.selectById(id);
}
}
直接在类的成员变量上使用注解(如@Autowired),由Spring容器直接将依赖对象注入到字段中,无需构造器或setter方法。这是最简洁的注入方式,但存在诸多隐患。
使用示例(看似简洁,实则有坑):
@Service
public class UserService {
// 字段注入:直接在字段上添加@Autowired
@Autowired
private UserDao userDao;
public User getUserById(Long id) {
return userDao.selectById(id);
}
}
️ 注意:Spring官方明确不推荐字段注入,建议优先使用构造器注入;如果有可选依赖,可结合setter注入使用。
| 注入方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 构造器注入 | 强制注入、可声明final、线程安全、测试友好 | 依赖过多时,构造器参数过长(可通过@Qualifier拆分) | 必填依赖(推荐首选) |
| Setter注入 | 可选注入、灵活性高、可动态修改 | 可能出现空指针、无法保证不可变性 | 可选依赖、需要动态修改依赖的场景 |
| 字段注入 | 代码简洁、开发高效 | 无法声明final、空指针风险、测试困难、易违反单一职责 | 临时测试、简单demo(不推荐生产使用) |
我们日常使用Spring时,只需添加@Autowired、@Service、@Repository等注解,Spring就会自动完成依赖注入。这背后的核心逻辑,其实是“Bean的创建 + 依赖解析 + 依赖注入”三个步骤,全程由Spring IoC容器(ApplicationContext)主导。
依赖注入的基础是“被依赖对象必须是Spring管理的Bean”——也就是说,被依赖类(如UserDaoImpl)必须通过@Component、@Service、@Repository等注解,或XML配置的方式,注册到Spring IoC容器中,成为Spring Bean。
Spring IoC容器会维护一个“Bean工厂”,负责创建Bean实例、管理Bean的生命周期(初始化、销毁),并对外提供Bean的获取方式。
Spring DI之所以能“无需手动创建对象”,核心依赖Java的反射机制——通过反射,Spring可以:
比如字段注入的底层反射逻辑(简化):
// 1. 获取UserService类的Class对象
Class<UserService> userServiceClass = UserService.class;
// 2. 获取UserService的实例(Spring创建)
UserService userService = userServiceClass.newInstance();
// 3. 获取userDao字段(private)
Field userDaoField = userServiceClass.getDeclaredField("userDao");
// 4. 打破访问权限限制
userDaoField.setAccessible(true);
// 5. 创建被依赖对象(UserDaoImpl),并注入到字段中
UserDao userDao = new UserDaoImpl();
userDaoField.set(userService, userDao);
实际开发中,除了基础的注入方式,我们还会遇到“多个Bean实现类”“依赖注入的优先级”“循环依赖”等问题,掌握以下进阶用法,能帮你应对各种场景。
当一个接口有多个实现类,且都被注册为Spring Bean时,Spring无法确定注入哪个实现类,会报“NoUniqueBeanDefinitionException”异常。此时,需用@Qualifier注解指定要注入的Bean的名称。
示例:
// 接口
public interface UserDao {
User selectById(Long id);
}
// 实现类1:Bean名称为userDaoMysql(默认是类名首字母小写)
@Repository
public class UserDaoMysqlImpl implements UserDao {
@Override
public User selectById(Long id) {
return new User(id, "MySQL查询:张三");
}
}
// 实现类2:Bean名称为userDaoRedis
@Repository("userDaoRedis") // 手动指定Bean名称
public class UserDaoRedisImpl implements UserDao {
@Override
public User selectById(Long id) {
return new User(id, "Redis查询:张三");
}
}
// 服务层:指定注入userDaoRedis
@Service
public class UserService {
private final UserDao userDao;
// @Qualifier指定Bean名称,与@Autowired配合使用
@Autowired
public UserService(@Qualifier("userDaoRedis") UserDao userDao) {
this.userDao = userDao;
}
}
如果一个接口有多个实现类,且我们希望“默认注入某个实现类”,可以在该实现类上添加@Primary注解——Spring会优先注入带有@Primary注解的Bean,无需每次都用@Qualifier指定。
示例:在UserDaoMysqlImpl上添加@Primary,那么默认会注入该实现类。
循环依赖指的是“两个或多个类互相依赖”,比如:UserService依赖UserDao,UserDao依赖UserService。如果处理不当,会导致死循环,最终报BeanCreationException异常。
️ 注意:Spring只能自动解决“构造器注入以外”的循环依赖(setter注入、字段注入),构造器注入的循环依赖,Spring无法解决,必须手动避免。
Spring解决循环依赖的核心原理:三级缓存(singletonObjects、earlySingletonObjects、singletonFactories),简单来说,就是“提前暴露未完成初始化的Bean实例”,让依赖方先获取到实例,避免死循环。
最佳实践:避免循环依赖——如果出现循环依赖,大概率是业务设计不合理,应通过“拆分服务”“引入中间层”等方式重构代码,而非依赖Spring的缓存机制。
有时候,我们需要在非Spring管理的类(比如手动new的类)中,使用Spring Bean。此时,可通过“实现ApplicationContextAware接口”或“手动获取Spring容器”的方式实现注入。
推荐方式(实现ApplicationContextAware):
// 1. 实现ApplicationContextAware,获取Spring容器
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
// 2. 提供静态方法,获取Spring Bean
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
// 3. 非Spring Bean中使用Spring Bean
public class NonSpringClass {
// 手动获取Spring Bean
private UserService userService = SpringContextUtil.getBean(UserService.class);
public void doSomething() {
User user = userService.getUserById(1L);
System.out.println(user);
}
}
依赖注入虽好,但如果使用不当,反而会引入新的问题。下面我们总结DI的核心优势,同时梳理实际开发中最容易踩的坑。
有些开发者为了“图方便”,将所有对象都交给Spring注入,甚至将工具类、常量类也注册为Bean,导致类的职责混乱,违背“单一职责原则”。
避坑:只有“需要被复用、需要解耦、需要参与业务逻辑”的对象,才注册为Spring Bean;工具类(如StringUtils)可采用静态方法,无需注入。
如前文所述,字段注入容易导致“手动创建类时,字段为null”的空指针异常,尤其是在非Spring管理的类中调用Spring Bean时。
避坑:优先使用构造器注入;如果必须使用字段注入,确保该类始终从Spring容器中获取(而非手动new)。
构造器注入的循环依赖,Spring无法解决,会报BeanCreationException异常;即使是setter注入的循环依赖,也会增加代码的复杂度和维护成本。
避坑:设计业务逻辑时,尽量避免循环依赖;如果出现循环依赖,可通过“拆分服务”“引入中间层”“将构造器注入改为setter注入”等方式解决。
Spring中的Bean默认是单例(singleton),即整个应用中只有一个实例。如果注入的Bean中包含状态变量(如成员变量count),多线程环境下会出现线程安全问题。
避坑:单例Bean中禁止使用状态变量;如果需要状态变量,将Bean的作用域改为原型(prototype),或使用ThreadLocal维护线程私有状态。
注入失败的常见原因:被依赖类未添加@Component、@Service等注解,或注解扫描包配置错误,导致Spring无法扫描到该Bean。
避坑:检查被依赖类是否添加了正确的注解;检查@ComponentScan注解的扫描包路径,确保包含被依赖类所在的包。
看到这里,相信你已经对Java依赖注入有了全面的理解。最后,我们用一句话总结DI的核心:
对于Java开发者来说,掌握DI不仅是掌握一种技术,更是掌握一种“松耦合”的架构思维:
依赖注入不是Spring的专属特性,而是一种通用的设计模式——即使不使用Spring,我们也可以手动实现简单的DI(如本文开头的模拟容器)。但在实际开发中,借助Spring等框架的DI能力,能让我们更专注于业务逻辑,提升开发效率。
希望本文能帮你真正吃透Java依赖注入,在后续的开发中,写出更优雅、更规范、更易维护的Java代码。如果有任何疑问或补充,欢迎在评论区交流~