Skip to content

IoC 与 DI(控制反转与依赖注入)

一、IoC 是什么

IoC = Inversion of Control(控制反转)

"Don't call me, we'll call you." — 好莱坞原则

传统方式:对象自己创建依赖

java
public class UserService {
    private UserDao userDao = new UserDaoImpl(); // 自己new
    
    public void save(User user) {
        userDao.save(user);
    }
}

IoC 方式:对象的依赖由外部注入

java
public class UserService {
    private UserDao userDao;  // 不自己创建
    
    public UserService(UserDao userDao) {  // 构造函数注入
        this.userDao = userDao;
    }
}
传统方式:
    UserService ──▶ UserDaoImpl (UserService 控制着 UserDao)
    
IoC方式:
    IoC Container ──▶ UserDaoImpl ──▶ UserService
                      (Container 统一管理依赖)

二、IoC 容器

Spring 的 IoC 容器负责:

  1. 创建对象
  2. 管理对象生命周期
  3. 注入对象依赖

主要容器

容器特点
BeanFactory基础接口,懒加载
ApplicationContext高级容器,功能更多
java
// BeanFactory - 基础容器(基本不用)
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
UserService service = factory.getBean("userService", UserService.class);

// ApplicationContext - 常用容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
UserService service = ctx.getBean("userService", UserService.class);

常见实现类

java
// 1. 从类路径加载 XML 配置
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

// 2. 从文件系统加载 XML 配置
FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("C:/config/beans.xml");

// 3. 注解方式(现代主流)
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

// 4. 扫描包路径
ctx.scan("com.example.*");

// 5. Spring Boot 方式
SpringApplication.run(Application.class, args); // 内部自动创建

三、DI 依赖注入

DI 是 IoC 的一种实现方式,指将依赖自动注入到对象中

1. 构造函数注入(推荐)

java
@Service
public class UserService {
    private final UserDao userDao;
    private final OrderService orderService;
    
    // 构造函数注入
    public UserService(UserDao userDao, OrderService orderService) {
        this.userDao = userDao;
        this.orderService = orderService;
    }
}

优点

  • 依赖不可变(final)
  • 初始化时依赖必须准备好
  • 便于单元测试

缺点

  • 构造函数参数过多时代码冗长

2. Setter 注入(可选)

java
@Service
public class UserService {
    private UserDao userDao;
    
    // setter 注入
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

适用场景:依赖可选的场景

3. 字段注入(简洁但不推荐)

java
@Service
public class UserService {
    @Autowired  // 不推荐,测试困难
    private UserDao userDao;
}

缺点

  • 无法注入不可变依赖
  • 难以单元测试(需要反射/容器)
  • 职责不清

4. @Bean 方法注入

java
@Configuration
public class AppConfig {
    
    @Bean
    public UserService userService() {
        return new UserService(userDao());
    }
    
    @Bean
    public UserDao userDao() {
        return new UserDaoImpl();
    }
}

四、注入方式选择

依赖项数量:少 ───────────────────────────────▶ 多
          │                                       │
          ▼                                       ▼
      构造函数注入                          Builder模式
                                          或 @ConfigurationProperties
          
可选依赖 ───────────────────────────────────────▶ 必选依赖
          │                                       │
          ▼                                       ▼
      Setter 注入                            构造函数注入

五、Bean 的配置方式

1. XML 配置(传统)

xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- 无参构造创建 -->
    <bean id="userService" class="com.example.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>
    
    <!-- 构造器参数 -->
    <bean id="orderService" class="com.example.OrderService">
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>
    
    <!-- 简单值注入 -->
    <bean id="demoService" class="com.example.DemoService">
        <property name="timeout" value="30"/>
        <property name="enable" value="true"/>
    </bean>
    
</beans>

2. 注解配置(现代主流)

java
// 定义 Bean
@Component          // 通用组件
@Service            // 业务层组件
@Repository         // 数据访问层组件
@Controller         // Web层组件
@RestController     // RESTful Web服务
public class UserService {
    // ...
}

// 注入依赖
@Autowired          // 按类型注入
@Resource(name="")  // 按名称注入
@Inject            // javax.inject 标准
private UserDao userDao;

3. Java Config 配置(推荐)

java
@Configuration
public class AppConfig {
    
    @Bean
    public UserService userService(UserDao userDao) {
        // 参数会自动注入
        return new UserService(userDao);
    }
    
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
        config.setUsername("root");
        config.setPassword("123456");
        return new HikariDataSource(config);
    }
}

六、Bean 的作用域

作用域说明线程安全
singleton单例,整个容器只有一个
prototype原型,每次获取创建新实例
request每次HTTP请求创建
session每次HTTP会话创建
applicationServletContext生命周期
websocketWebSocket生命周期
java
// 单例(默认)
@Service
@Scope("singleton")  // 或 @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class UserService { }

// 原型
@Service
@Scope("prototype")  // 每次注入创建新对象
public class MyProtoBean { }

// 请求域
@RequestScope
public class RequestBean { }

// 会话域
@SessionScope
public class SessionBean { }

单例 vs 原型对比

java
@Service
@Scope("singleton")
public class SingletonBean {
    private int count = 0;
    public void increment() { count++; }
    public int getCount() { return count; }
}

@Service
@Scope("prototype")
public class PrototypeBean {
    private int count = 0;
   usan void increment() { count++; }
    public int getCount() { return count; }
}

// 测试
SingletonBean s1, s2;  // s1 和 s2 共享同一个 count
PrototypeBean p1, p2;  // p1 和 p2 各有独立的 count

七、延迟加载 Bean

java
// 1. @Lazy 注解(单次使用)
@Service
@Lazy
public class LazyBean {
    public LazyBean() {
        System.out.println("LazyBean created!");
    }
}

// 2. BeanFactory 懒加载
@Bean
@Lazy
public UserService userService() {
    return new UserService();
}

// 3. 全局懒加载(Spring Boot 2.x+)
// application.yml
spring:
  main:
    lazy-initialization: true

// 4. @ConditionalOnLazy(条件懒加载)
@Bean
@ConditionalOnLazy
public UserService lazyUserService() {
    return new UserService();
}

八、循环依赖问题

什么是循环依赖

java
@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;  // 循环!
}

Spring 如何解决

Spring 三级缓存:
┌─────────────────────────────────────────────────────────────┐
│  singletonObjects     │ 第一级 │ 完整Bean实例               │
│  earlySingletonObjects│ 第二级 │ 早期暴露的Bean(未完成DI) │
│  singletonFactories   │ 第三级 │ Bean工厂                  │
└─────────────────────────────────────────────────────────────┘

解决流程:
1. A 创建,发现依赖 B
2. 去一级缓存找 B,没找到
3. 创建 B 的早期引用,提前暴露
4. B 创建,发现依赖 A
5. 从缓存拿到 A 的早期引用
6. B 创建完成,存入一级缓存
7. A 完成创建,存入一级缓存

构造器循环依赖无法解决

java
// 构造器注入的循环依赖 Spring 无法处理
@Service
public class A {
    public A(B b) { }  // 构造器循环依赖
}

@Service
public class B {
    public B(A a) { }  // Spring 启动失败!
}

// 解决方案:改用 setter 注入
@Service
public class A {
    private B b;
    @Autowired
    public void setB(B b) { this.b = b; }
}

九、面试高频问题

Q1: IoC 和 DI 是什么关系?

  • IoC(控制反转):思想,对象控制权反转到容器
  • DI(依赖注入):IoC 的一种具体实现方式

Q2: @Autowired 和 @Resource 区别?

对比@Autowired@Resource
来源SpringJSR-250
匹配方式类型为主名称为主
required可设置不可设置
位置字段/构造函数/setter字段/setter
java
// @Autowired 先按类型找,找不到再按名称
@Autowired
private UserDao userDao;  // 先找 UserDao 类型

// @Resource 先按名称找,找不到再按类型
@Resource
private UserDao userDao;  // 先找 "userDao" 名称

Q3: BeanFactory 和 ApplicationContext 区别?

对比BeanFactoryApplicationContext
加载方式懒加载预加载
功能基础增强(事件、国际化等)
常用度基本不用主流

Q4: 为什么建议构造器注入?

  1. 依赖不可变(final)
  2. 保证了依赖不为 null
  3. 更容易测试
  4. 明确声明了所有依赖

Q5: prototype Bean 什么时候销毁?

  • singleton:容器关闭时销毁
  • prototype:对象无人引用时由 GC 回收,Spring 不管理其生命周期

十、代码练习

java
// 1. 创建项目,引入 spring-context
// 2. 定义接口和实现
public interface MessageService {
    String getMessage();
}

@Service
public class EmailService implements MessageService {
    @Override
    public String getMessage() {
        return "Email: Hello!";
    }
}

@Service
public class SmsService implements MessageService {
    @Override
    public String getMessage() {
        return "SMS: Hello!";
    }
}

// 3. 构造函数注入
@Service
public class UserNotifier {
    private final MessageService messageService;
    
    public UserNotifier(MessageService messageService) {
        this.messageService = messageService;
    }
    
    public void notify() {
        System.out.println(messageService.getMessage());
    }
}

// 4. 测试
@Configuration
@ComponentScan("com.example")
public class TestConfig { }

public class Main {
    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(TestConfig.class);
        var notifier = ctx.getBean(UserNotifier.class);
        notifier.notify();
    }
}

十一、下一章预告

下一章我们将学习 AOP(面向切面编程),了解:

  • AOP 核心概念(切面、连接点、通知)
  • 5种通知类型
  • 基于注解和 XML 的 AOP 配置
  • Spring AOP 与 AspectJ 的区别

基于 MIT 许可发布