Spring IoC 深度解析:从依赖注入最佳实践到底层循环依赖机制

18 阅读7分钟

IoC 是什么

管理 Spring Bean 的容器,IoC 即“控制反转”,依靠“依赖注入(DI)”实现。
更准确地来说:IoC 是一种思想,而 DI 是一种实现,DI 实现了 IoC

“对象的创建”这一行动,由原本的程序员(我们)手上,转移到了 IoC 的手上


三种配置方式和三种注入方式

第一组:XML 配置模式

特点:类是纯净的 POJO,没有任何 Spring 注解,所有逻辑都在 applicationContext.xml 中。

1. XML + 构造器注入(旧代码)

配置代码 (applicationContext.xml)

<!-- 1. 定义依赖 -->
<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>

<!-- 2. 定义业务类,使用 constructor-arg -->
<bean id="userService" class="com.example.service.impl.UserServiceImpl">
    <constructor-arg ref="userDao"/>
</bean>

UserServiceImpl 类代码

package com.example.service.impl;
import com.example.dao.UserDao;

public class UserServiceImpl implements UserService {
    private final UserDao userDao;

    // 必须提供带参构造器
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

2. XML + Setter 注入

配置代码 (applicationContext.xml)

<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>

<bean id="userService" class="com.example.service.impl.UserServiceImpl">
    <!-- 使用 property 标签,name 对应 set 方法后的属性名 -->
    <property name="userDao" ref="userDao"/>
</bean>

UserServiceImpl 类代码

package com.example.service.impl;
import com.example.dao.UserDao;

public class UserServiceImpl implements UserService {
    private UserDao userDao;

    // 必须有 Setter 方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

3. XML + 注解注入

注意:XML 配置不支持注解注入。XML 容器无法通过反射直接赋值,所以这种组合不存在。

第二组:Java 配置模式 (Java Config)

特点:使用 @Configuration 和 @Bean,类通常是普通的(也可以加注解,但这里演示纯 Java 配置写法)。

4. Java Config + 构造器注入(一般推荐)

配置代码 (AppConfig.java)

@Configuration
public class AppConfig {

    @Bean
    public UserDao userDao() {
        return new UserDaoImpl();
    }

    // 在 @Bean 方法的参数中声明依赖,Spring 会自动注入
    @Bean
    public UserService userService(UserDao userDao) {
        // 显式调用构造器
        return new UserServiceImpl(userDao);
    }
}

UserServiceImpl 类代码

package com.example.service.impl;
import com.example.dao.UserDao;

// 这是一个普通类,不需要 @Service
public class UserServiceImpl implements UserService {
    private final UserDao userDao;

    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

5. Java Config + Setter 注入

配置代码 (AppConfig.java)

@Configuration
public class AppConfig {

    @Bean
    public UserDao userDao() { return new UserDaoImpl(); }

    @Bean
    public UserService userService() {
        UserServiceImpl service = new UserServiceImpl();
        // 手动调用 Setter 方法注入
        service.setUserDao(userDao()); 
        return service;
    }
}

UserServiceImpl 类代码

package com.example.service.impl;
import com.example.dao.UserDao;

public class UserServiceImpl implements UserService {
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

6. Java Config + 注解注入

注意:在纯 Java Config 模式下,我们通常不通过注解注入来管理 Bean,而是通过方法参数或返回值。
如果非要这么干,必须先在类上加 @Autowired 注解(这就变成了混合模式),纯 Java Config 模式不支持字段注入。

第三组:注解配置模式 (Annotation)

特点:类上加 @Service,配合 @Autowired,Spring 自动扫描。这是 Spring Boot 的主流。

7. 注解 + 构造器注入(官方推荐)

配置代码
无需显式配置,Spring Boot 启动类扫描包即可。

UserServiceImpl 类代码

@Service // 1. 注册 Bean
public class UserServiceImpl implements UserService {

    private final UserDao userDao;

    // 2. 构造器注入
    // 如果只有一个构造器,@Autowired 可以省略
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

8. 注解 + Setter 注入

配置代码
无需显式配置。

UserServiceImpl 类代码

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    // @Autowired 写在 Setter 方法上面
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void findAll() { userDao.query(); }
}

9. 注解 + 字段注入(最常用但不推荐)

配置代码
无需显式配置。

UserServiceImpl 类代码

@Service
public class UserServiceImpl implements UserService {

    // @Autowired 直接写在字段上
    @Autowired
    private UserDao userDao;

    public void findAll() { userDao.query(); }
}

核心难点:如何处理多个实现类?

当 UserDao 有两个实现类 UserDaoImpl 和 AdminUserDaoImpl 时,Spring 不知道该选谁,会报错 NoUniqueBeanDefinitionException

1. XML 配置下的解决方案

XML 依靠 ref 属性天然就是 byName 的,即对应了Bean的id,所以非常清晰。

<!-- 定义两个实现类 -->
<bean id="userDao" class="...UserDaoImpl"/>
<bean id="adminUserDao" class="...AdminUserDaoImpl"/>

<!-- 在注入时,直接修改 ref 指向你想要的 ID -->
<bean id="userService" class="...UserServiceImpl">
    <property name="userDao" ref="adminUserDao"/>
</bean>

2. Java Config 下的解决方案

使用 @Qualifier 或者方法名匹配。

@Bean
public UserService userService(UserDao adminUserDao) { // 参数名必须匹配 Bean 的名字
    return new UserServiceImpl(adminUserDao);
}
// 或者
@Bean
public UserService userService(@Qualifier("adminUserDao") UserDao userDao) {
    return new UserServiceImpl(userDao);
}

3. 注解配置下的解决方案

必须使用 @Qualifier 注解来指定 Bean 的名字(默认类名首字母小写)。

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;

    @Autowired
    public UserServiceImpl(@Qualifier("adminUserDao") UserDao userDao) {
        this.userDao = userDao;
    }
}

对照表

组合模式配置位置注入代码特征类代码特征推荐度
XML + 构造器<constructor-arg>调用带参构造器纯 POJO,有构造器(老项目)
XML + Setter<property>调用 set 方法纯 POJO,有 Setter
Java + 构造器@Bean 方法参数new Obj(param)纯 POJO,有构造器(第三方库)
Java + Setter方法内调用 setsetXxx(param)纯 POJO,有 Setter
注解 + 构造器@Service(自动注入)@Service,有构造器(首选)
注解 + Setter@Autowired(自动注入)@Service,有 Setter
注解 + 字段@Autowired(自动注入)@Service,私有字段(最常用)

三种注入方式评估

setter 注入

缺点:

  1. 空指针风险,在注入前必须先构造,存在为空的半结构化风险。
  2. 无法使用final修饰,对象为动态,是不稳定的。
  3. 无法发现循环依赖问题,默认通过。
  4. Java Config 配置时需要手动调用 setter 方法

优点:

  1. 灵活性高。

Java Config 注入

缺点:

  1. 修改配置需重新编译、重新部署。
  2. 没有注解+构造的模式方便。

优点:

  1. 在java文件中写,类型安全,有编译期检查。
  2. 逻辑能力强。
  3. 允许 @ComponentScan 注解进行扫包,将外部库注册为 Bean

构造器注入

缺点:

  1. 构造函数参数过多,违反了单一职责原则,需要拆分。

优点:

  1. 暴露循环依赖,会直接报错,强制你进行解决,而不是等待某个业务出问题才报错,提高了稳定性。
  2. final 支持不可变性。
  3. 类不依赖 Spring 容器,完全是一个普通的 Java 对象,意味着没有 Spring 容器时也能运行。

综上,构造器注入为一般情况下最优的依赖注入,通常配合注解配置使用
若需求第三方库时,无法通过添加注解直接对代码更改,需要使用 构造器注入 + Java Config配置

AOP 下的循环依赖问题

Aop会使对象产生代理对象,Spring 在三级缓存的工厂里会生成代理对象,而不是原始对象。

对于两个代理对象的互相注入,会出现问题。

对于两个类:class A 和 class B
当A中进行B的依赖注入,同时B中也进行A的依赖注入,Spring 会无法操作从而出现错误

解决方案:三级缓存

注意:三级缓存也无法解决构造器注入的循环以来问题

三级缓存:为了解决Aop代理问题

第三级缓存: 存放注册Bean的逻辑(即工厂,ObjectFactory,用来生成某个对象的 Bean ),还未实例化
第二级缓存: 存放半实例化的对象,此时为半成品
第一级缓存: 存放实例化的对象(若存在Aop则为代理后的对象ProxyA,并非原始对象),此时为完整品

三级缓存的行为模拟

  1. 当 A 进行注册时,Spring 将 “ A 的工厂 ” (ObjectFactory) 放入 第三级缓存。
  2. 发现 A 的实例化需要注入 B 。
  3. 在所有缓存中寻找 B,发现没找到实例化的 B 。
  4. 进行B的注册,Spring 将 “ B 的工厂 ” 放入第三级缓存。
  5. 发现 B 的实例化需要注入 A 。
  6. 在缓存中寻找 A,发现没找到实例化的 A 。
  7. B 去三级缓存找 A 的工厂,工厂发现 A 需要 AOP,于是提前生成 ProxyA,并将 ProxyA 放入二级缓存。
  8. 此时 A 为半实例化, B 将 ProxyA 进行注入后,生成 B 的代理对象ProxyB,转移到第一级缓存中,并且删除第三级缓存的 ProxyB 。
  9. B 实例化完毕,第二级缓存的 ProxyA 将 ProxyB 注入, ProxyA 从第二级缓存转移到第一级缓存中。并删除第二级缓存的 ProxyA。
  10. 此时 ProxyA 和ProxyB 都在第一级缓存中,完毕。

构造器注入的具体解决方法:@Lazy

两个方法:

  1. 使用@Lazy懒加载,注入一个延迟代理。
  2. 重构代码,拆分出第三个类 C , A 和 B 都依赖 C。

本文结束
如果这篇文章帮你理清了思路,我也感到很开心。
这也同时是我的学习笔记,能帮到别人我不胜感谢,若哪里出错希望指出,同样不胜感激!

以上,@Aroaku