IoC(控制反转):把「创建对象、组装依赖」的控制权,从你的代码反转给 Spring 容器。你只声明「我需要一个 EmailSender」,容器造好后**注入(DI)**给你。对象之间从「硬编码焊接」变成「接口插拔」,这正是分层架构能解耦的基础。本站后端的 Service 层依赖 Repository、Controller 依赖 Service,全部走容器注入。
// 痛点演示:业务代码硬编码依赖实现
public class TaskService {
// 想换 SmsSender?只能改这一行源码,重新编译。
// 写单测想 mock 掉发邮件?没办法,它是在构造时 new 死的。
private final EmailSender emailSender = new EmailSender();
}
先定义接口和两个实现,作为本课的领域模型:
package com.example.taskflow.notify;
public interface NotificationSender {
String channel();
void send(String to, String content);
}
package com.example.taskflow.notify;
import org.springframework.stereotype.Component;
@Component // 登记进容器
public class EmailSender implements NotificationSender {
@Override
public String channel() { return "email"; }
@Override
public void send(String to, String content) {
System.out.println("[邮件] 发给 " + to + ":" + content);
}
}
package com.example.taskflow.notify;
import org.springframework.stereotype.Component;
@Component
public class SmsSender implements NotificationSender {
@Override
public String channel() { return "sms"; }
@Override
public void send(String to, String content) {
System.out.println("[短信] 发给 " + to + ":" + content);
}
}
步骤 2:构造器注入(标准姿势)
新建 Service,注意只用构造器参数声明依赖,不加任何 @Autowired:
package com.example.taskflow.service;
import com.example.taskflow.notify.NotificationSender;
import com.example.taskflow.notify.SmsSender;
import org.springframework.stereotype.Service;
@Service
public class TaskService {
private final NotificationSender sender; // final:构造后不可变
// 单一实现时可省略 @Autowired;这里有两个实现,需要指明
public TaskService(@Qualifier("smsSender") NotificationSender sender) {
this.sender = sender;
}
public void notifyUser(String user) {
sender.send(user, "你有新的任务待处理");
}
}
// Controller 里同样构造器注入,链条串起来
@RestController
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/api/tasks/notify")
public String notify() {
taskService.notifyUser("zhangsan");
return "已通知";
}
}
访问 /api/tasks/notify,控制台输出 [短信] 发给 zhangsan:你有新的任务待处理。整个过程你没有 new 过任何东西——容器先创建 SmsSender,再创建 TaskService 时把它从构造器塞进去。
启动报 Parameter 0 of constructor in xxx required a bean of type 'yyy' that could not be found:被依赖的类没进容器。三查:类上有无 @Component 系注解;包是否在启动类扫描范围内;是第三方类的话改用 @Bean 注册。
启动报 expected single matching bean but found 2:多实现注入冲突。注入点加 @Qualifier 指定 Bean 名(默认为类名首字母小写),或在一个实现上加 @Primary。
启动报 bean cycle:读报错里的环路径 a -> b -> a,重构抽公共类;确需保留则在一端构造器参数加 @Lazy。
注入的对象运行时是 null:确认访问路径真的经过容器(反射 new 出来的、static 工具类里的注入都不行);工具类需要依赖时,注册为 Bean 而不是静态方法。
面试常问
什么是 IoC 和 DI?带来了什么好处?
控制反转:对象的创建与依赖组装交给容器。好处:解耦(面向接口可替换)、可测试(mock 注入)、生命周期统一管理、与 AOP/事务等容器能力天然协同。
为什么推荐构造器注入而不是字段注入?
依赖不可变(final)、依赖完整性强(构造即可用,缺依赖启动即失败)、不依赖容器也能 new 出来单测、利于发现「依赖过多」的设计问题。
Spring 怎么解决循环依赖?(三级缓存)
一级缓存放成品 Bean;二级缓存放提前暴露的早期引用;三级缓存放 ObjectFactory 用于必要时生成早期代理。A 实例化后先把工厂放进三级缓存再填充属性,B 需要 A 时从三级缓存拿到早期引用,打破环。(注意:仅限单例 + 字段/Setter 注入路径,构造器循环依赖无解,Boot 3 默认关闭自动容忍。)