Skip to content

IoC 容器与依赖注入 ⚠️

这是 Spring Boot 最核心的概念,也是 PHP 开发者最难适应的地方之一。


核心思想

依赖注入(DI)在 PHP 中很简单:

php
// PHP:手动控制
$db = new Database($config);
$repo = new UserRepository($db);
$service = new UserService($repo);
$controller = new UserController($service);

Spring Boot 的做法是"反转控制"(IoC):

java
// 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

java
// 方式一:注解(最常用)
@Service                              // 业务逻辑层
public class UserService { ... }

@Repository                           // 数据访问层
public class UserRepository { ... }

@Component                            // 通用组件
public class EmailService { ... }

@RestController                       // 控制器
public class UserController { ... }
java
// 方式二:@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 完全一样,只是语义更明确。选择哪个取决于类本身的角色。


注入方式

java
// 方式一:构造器注入 ✅ 推荐
@RestController
public class UserController {
    private final UserService userService;      // final 确保不可变

    public UserController(UserService userService) {
        this.userService = userService;
    }
}
java
// 方式二:字段注入(不推荐,但很多老代码在用)
@RestController
public class UserController {

    @Autowired                              // 依赖注入注解
    private UserService userService;         // 不能加 final

    @Autowired
    private EmailService emailService;
}
java
// 方式三:Setter 注入(不常用)
@RestController
public class UserController {
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

⚠️ 建议只用构造器注入。原因:

  1. final 保证不可变,线程安全
  2. 显式声明依赖,不隐藏
  3. 单元测试可以直接 new UserController(mockService),不需要反射

扫描机制

java
// 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.controllercom.example.service 等都会被扫描到。


⚠️ 常见坑

1. 循环依赖

java
@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

解决方案:

  1. 重新设计,抽出公共部分到第三个 Service
  2. 其中一个用 @Lazy 延迟初始化
  3. @Autowired 字段注入打破(不推荐,掩盖了设计问题)

2. new 出来的对象不会被 DI

java
UserService service = new UserService();      // ❌ Spring 不管理
// service 内部的 @Autowired 字段会全部为 null

正确做法:所有 Bean 都通过 DI 获取。

java
@Autowired
private UserService userService;              // ✅

3. 不是所有类都要注册为 Bean

java
// DTO、VO、Entity 这些纯数据类不需要任何注解
public class User {
    private String name;
    private String email;
    // 只有 getter/setter,不需要 @Component
}

只有"需要被管理"的类才需要注册为 Bean——通常是 Service、Repository、Controller、Config 等有状态的组件。DTO、Entity 等数据容器由 new 创建。

4. 单例陷阱

java
@Service
public class CounterService {
    private int count = 0;  // ❌ 所有请求共享这个变量

    public int increment() {
        return ++count;
    }
}

Bean 默认是单例(Singleton),所有请求共用一个实例。这意味着不能在其中放请求级别的状态变量。这一点和 PHP-FPM 完全不同——PHP 中每个请求都是独立进程/线程,类变量每次请求都是全新的。

面向 PHP 开发者的 Spring Boot 文档