IoC 容器与依赖注入 ⚠️
这是 Spring Boot 最核心的概念,也是 PHP 开发者最难适应的地方之一。
核心思想
依赖注入(DI)在 PHP 中很简单:
// PHP:手动控制
$db = new Database($config);
$repo = new UserRepository($db);
$service = new UserService($repo);
$controller = new UserController($service);Spring Boot 的做法是"反转控制"(IoC):
// Java:你只需要声明"我要什么",容器帮你组装
@RestController
public class UserController {
private final UserService userService;
// Spring 自动找到 UserService 的实现并注入进来
public UserController(UserService userService) {
this.userService = userService;
}
}你不需要写
new,不需要手动组装依赖树。 Spring IoC 容器在启动时扫描所有@Component/@Service/@Repository/@Controller,创建实例并管理它们的生命周期。
注册 Bean
// 方式一:注解(最常用)
@Service // 业务逻辑层
public class UserService { ... }
@Repository // 数据访问层
public class UserRepository { ... }
@Component // 通用组件
public class EmailService { ... }
@RestController // 控制器
public class UserController { ... }// 方式二:@Bean 方法(适合第三方类)
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() { // 相当于 new RestTemplate()
return new RestTemplate(); // 注册为一个 Bean
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service/@Repository/@Controller都是@Component的特化版本,功能和@Component完全一样,只是语义更明确。选择哪个取决于类本身的角色。
注入方式
// 方式一:构造器注入 ✅ 推荐
@RestController
public class UserController {
private final UserService userService; // final 确保不可变
public UserController(UserService userService) {
this.userService = userService;
}
}// 方式二:字段注入(不推荐,但很多老代码在用)
@RestController
public class UserController {
@Autowired // 依赖注入注解
private UserService userService; // 不能加 final
@Autowired
private EmailService emailService;
}// 方式三:Setter 注入(不常用)
@RestController
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}⚠️ 建议只用构造器注入。原因:
- 用
final保证不可变,线程安全- 显式声明依赖,不隐藏
- 单元测试可以直接
new UserController(mockService),不需要反射
扫描机制
// DemoApplication.java
@SpringBootApplication // 包含 @ComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication会扫描 当前包及其子包 中所有带@Component(及其派生注解)的类,自动注册为 Bean。所以入口类必须放在包的根路径。如果入口在
com.example,则com.example.controller、com.example.service等都会被扫描到。
⚠️ 常见坑
1. 循环依赖
@Service
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) { // 循环依赖
this.orderService = orderService;
}
}
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) { // 循环依赖
this.userService = userService;
}
}
// 启动报错:Requested beans are currently in creation解决方案:
- 重新设计,抽出公共部分到第三个 Service
- 其中一个用
@Lazy延迟初始化- 用
@Autowired字段注入打破(不推荐,掩盖了设计问题)
2. new 出来的对象不会被 DI
UserService service = new UserService(); // ❌ Spring 不管理
// service 内部的 @Autowired 字段会全部为 null正确做法:所有 Bean 都通过 DI 获取。
java@Autowired private UserService userService; // ✅
3. 不是所有类都要注册为 Bean
// DTO、VO、Entity 这些纯数据类不需要任何注解
public class User {
private String name;
private String email;
// 只有 getter/setter,不需要 @Component
}只有"需要被管理"的类才需要注册为 Bean——通常是 Service、Repository、Controller、Config 等有状态的组件。DTO、Entity 等数据容器由
new创建。
4. 单例陷阱
@Service
public class CounterService {
private int count = 0; // ❌ 所有请求共享这个变量
public int increment() {
return ++count;
}
}Bean 默认是单例(Singleton),所有请求共用一个实例。这意味着不能在其中放请求级别的状态变量。这一点和 PHP-FPM 完全不同——PHP 中每个请求都是独立进程/线程,类变量每次请求都是全新的。