Spring Boot 里的 IOC 和 DI,其实没那么玄
刚学 Spring 的时候,看到 @Autowired 一脸懵,后来才明白控制反转就是把 new 对象这事外包给了容器。
Spring Boot 里的 IOC 和 DI,其实没那么玄
记得刚接触 Spring 的时候,第一反应是这东西在干嘛。
传统 Java 开发,我要用某个类,直接 new 不就完了吗?为什么要搞个什么「容器」出来,还要加各种注解?
后来学了一段时间才反应过来,原来那些注解背后藏着的是两件事:IOC 和 DI。
说白了,就是 Spring 帮我把对象的创建和管理这事给包了。
没有 Spring 的时候
回想一下,不用 Spring 的时候,代码大概长这样:
public class UserController {
private UserService userService = new UserServiceImpl();
public void getUser() {
userService.getUser();
}
}
看起来挺正常的对吧,我要用 UserService,就 new 一个。
但问题很快就来了。
比如,我想换个实现,改成 VIPUserService,怎么办,改代码。
比如,我想写单元测试,Mock 掉 UserService,怎么办,还是改代码。
再比如,如果 UserService 依赖了 Mapper,Mapper 依赖了 DataSource,一层层 new 下去,代码就乱成一锅粥了。
这时候你就懂了,耦合度高得离谱。
有了 Spring 之后
Spring 的出现,本质上就是为了解决这个问题。
它的思路很简单:
别自己 new 了,我把对象给你创建好,你直接用就行了。
@RestController
public class UserController {
@Autowired
private UserService userService;
public void getUser() {
userService.getUser();
}
}
就加了这么一行 @Autowired,Spring 会自动把我需要的 UserService 给我注入进来。
我不需要关心这个对象是怎么创建的,也不用担心它依赖了什么。
这就是 IOC,控制反转。
到底反转了什么?
听起来有点抽象,用个生活例子来说明一下。
传统方式 就像你自己做饭:
- 自己去买菜 → 自己洗菜 → 自己炒菜 → 自己洗碗
- 每个步骤都要自己控制
IOC 方式 就像去餐厅吃饭:
- 你只负责点菜(声明需要什么)
- 餐厅负责买菜、洗菜、炒菜、上菜(Spring 容器负责创建和管理对象)
- 你不需要关心菜是怎么做的
所谓「控制反转」,反转的就是对象创建权。
以前是程序员控制对象的创建(自己 new),现在是 Spring 容器控制对象的创建(自动注入)。
控制权从程序员手里,反转到了容器手里。
DI 又是什么?
经常会听到有人把 IOC 和 DI 分开讲,搞得好像两个东西一样。
其实它们说的是同一件事,只是角度不同。
- IOC(控制反转) 是思想:对象的创建和管理权交给容器
- DI(依赖注入) 是实现:容器自动把依赖的对象注入进来
举个更具体的例子:
@RestController
public class UserController {
@Autowired
private UserService userService;
}
这里的 @Autowired 就是 DI 的实现。
Spring 在启动的时候,会:
- 扫描到
UserServiceImpl上的@Service,创建这个 Bean - 扫描到
UserController上的@RestController,创建这个 Bean - 发现
UserController中有@Autowired的UserService - 从容器中找到
UserService类型的 Bean,注入到UserController中
整个过程对程序员透明,你只需要写注解就行。
三种注入方式,哪种最好?
Spring 提供了三种注入方式,各有各的用法。
1. 字段注入(最常用)
@Autowired
private UserService userService;
写法最简单,这也是为什么很多教程和开源项目都用这种方式。
但有个小问题:不利于单元测试。因为你没法通过构造器传入 Mock 对象。
2. 构造器注入(推荐)
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
或者更简洁的写法(只有一个构造器时,@Autowired 可以省略):
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
这种写法的好处是:
- 依赖关系清晰,一眼就能看出这个类依赖了什么
- 可以用
final修饰,保证不可变 - 便于单元测试
这也是官方推荐的写法。
3. Setter 注入
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
可以在运行时动态修改依赖,但有个缺点:对象可能处于不完整状态(依赖还没注入就被使用)。
所以一般用得比较少。
Bean 到底是什么?
学 IOC/DI 的时候,总会听到「Bean」这个词。
其实它很简单:Bean 就是被 Spring 容器管理的对象。
以前你自己 new 出来的对象,现在变成了 Spring 帮你创建和管理的对象,所以叫 Bean。
给对象加注解,Spring 就会把它当成 Bean 管理起来:
@Service // 业务层 Bean
public class UserServiceImpl implements UserService {
// ...
}
@Repository // 数据访问层 Bean
public class UserMapperImpl implements UserMapper {
// ...
}
@Component // 通用组件 Bean
public class JwtUtils {
// ...
}
@Service、@Controller、@Repository 本质上都是 @Component 的变体。
用不同的注解是为了语义清晰,让人一看就知道这个类属于哪一层。
那第三方库的 Bean 怎么加?
有时候要用一些第三方库的对象,比如 RestTemplate、ObjectMapper 之类的。
这些类不是我们写的,没法加 @Component 注解,怎么办?
用 @Bean 注解在配置类里定义:
@Configuration
public class MyConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
return mapper;
}
}
@Bean 用在方法上,方法名默认就是 Bean 的名称。
这样第三方对象也能被 Spring 管理了。
一个小坑:循环依赖
学 IOC 的时候,会遇到一个经典问题:循环依赖。
就是 A 依赖 B,B 又依赖 A,形成一个环:
@Service
public class A {
@Autowired
private B b; // A 依赖 B
}
@Service
public class B {
@Autowired
private A a; // B 依赖 A → 循环!
}
这时候 Spring 启动就会报错:
The dependencies of some of the beans in the application context form a cycle
解决方案:
- 最好的方式是重新设计,打破循环依赖
- 实在不行,可以用
@Lazy延迟加载其中一个依赖
@Service
public class B {
@Autowired
@Lazy
private A a; // 延迟加载,等真正使用时再注入
}
一句话总结
IOC 和 DI 是 Spring 框架的基石,理解了它们,就理解了 Spring 的核心思想:把对象的创建和管理交给容器,程序员只关心业务逻辑。
口诀我也背过:
IOC 反转控制权,DI 注入靠容器; Component 扫组件,Bean 注解建第三方; Autowired 按类型,Resource 按名称; 构造注入最推荐,单例多例看场景。
刚开始学的时候觉得这些概念很抽象,后来自己写项目碰壁了才真正理解。
现在回头看,其实没啥大不了的,就是 Spring 帮你把 new 对象这事给包了而已。