容器与依赖注入
Laravel 里你见过「服务容器」和「依赖注入」:
app(UserService::class)从容器里取对象,构造函数类型提示让容器自动注入。Spring 的 IoC 容器是同一套思想,只是默认全部用注解声明,而且「容器里的对象」有个专属名字叫 Bean。
什么是 IoC 容器
IoC(Inversion of Control,控制反转):对象的创建和生命周期管理,不归你管,归容器管。
对比一下:
// Laravel / PHP:自己 new,或从容器取
$service = new UserService(new UserRepository());
// 或
$service = app(UserService::class);// Spring:只需要声明"我要一个 UserService"
@Service
public class PostService {
private final PostRepository repository;
public PostService(PostRepository repository) { // 构造函数声明需求
this.repository = repository;
}
}注意:PostService 自己从不 new PostRepository,也不从什么全局函数取。它只是「声明需求」,容器在创建它时自动把 PostRepository 传进来。这就是依赖注入(DI)。
为什么叫"反转"?
在 Laravel/Spring 里,不是你的代码去「拉取」依赖(new、app()),而是容器「推送」依赖进来(构造器参数)。控制权反转了,所以叫控制反转。
容器里的对象叫 Bean
容器里管理着成千上万的对象,统称 Bean。把类交给容器,叫「注册为 Bean」。Spring Boot 里三种最常用的注册方式:
| 注解 | 用途 | 类比 Laravel |
|---|---|---|
@Component | 通用组件 | 让容器管理这个类 |
@Service | 业务服务层(语义更清晰) | 其实也是 Component |
@Repository | 数据访问层(自带异常转换) | 也是 Component |
@Controller / @RestController | 控制器 | 也是 Component |
@Configuration + @Bean | 手动装配一个对象 | 在 ServiceProvider 的 register() 里绑定 |
@Service
public class PostService { ... }加上 @Service,这个类的实例就归容器管了,任何地方需要它都会被自动注入。相当于 Laravel 里被容器管理的服务类(可能加 singleton() 绑定的那种)。
三种注入方式
1. 构造器注入(推荐)
@Service
public class PostService {
private final PostRepository repository;
private final CacheService cache;
public PostService(PostRepository repository, CacheService cache) {
this.repository = repository;
this.cache = cache;
}
}- 依赖不可变(
final),对象创建时依赖就齐了 - 无法构造出不完整的对象,空指针风险最低
- 测试时直接手动
new PostService(mockRepo, mockCache)即可,不需要容器
2. 字段注入
@Service
public class PostService {
@Autowired
private PostRepository repository;
}写起来最短,但字段可能为 null、没法 final、测试要借助 Spring 容器。团队协作建议不用。
3. Setter 注入
@Autowired
public void setRepository(PostRepository repository) {
this.repository = repository;
}用于依赖可变的场景,用得少。
只有一个构造器时可以省略 @Autowired
Spring 4.3 之后,只要类只有一个构造器,容器会自动用它的参数做注入,@Autowired 可以不写。上面的 PostService 就是这么工作的。
从容器取对象:类似 app()
PHP 里你随时 app(SomeClass::class);Java 里 Spring 也允许手动取,只是不太鼓励:
@RestController
public class DemoController {
@Autowired
private ApplicationContext context; // 容器本体
@GetMapping("/fetch")
public Object fetch() {
return context.getBean(PostService.class); // ≈ app(PostService::class)
}
}日常开发你会用容器吗?几乎不会。因为注入就够了。手动 getBean 相当于 Laravel 里到处 app() 的坏味道,能少用就少用。
一个接口多个实现怎么办
Laravel 用「接口 + 绑定哪个实现」解决;Spring 用 @Qualifier 或「按名字」指定:
public interface PaymentGateway {
void pay(BigDecimal amount);
}
@Service
@Qualifier("alipay")
public class AlipayGateway implements PaymentGateway { ... }
@Service
@Qualifier("wechat")
public class WechatGateway implements PaymentGateway { ... }注入时指名要哪个:
@Service
public class OrderService {
private final PaymentGateway gateway;
public OrderService(@Qualifier("wechat") PaymentGateway gateway) {
this.gateway = gateway;
}
}对照 Laravel
Laravel:$this->app->bind(PaymentGateway::class, WechatPay::class)。Spring:接口 + @Qualifier("名字")。思想一致:面向接口编程,具体实现交给容器。
默认生命周期:单例
Laravel 的容器绑定默认是 singleton;Spring 的 Bean 默认也是单例(一个类在容器里只有一个实例,全局共享)。这一点对并发、状态、性能影响巨大,详见 Bean 作用域与生命周期。
依赖注入解决了什么问题
| 场景 | 没有 DI 的写法 | 有 DI 的写法 |
|---|---|---|
| 测试时换掉数据库访问 | 改生产代码,或用复杂 mock | new PostService(mockRepository) 一行搞定 |
| 换支付渠道实现 | 改所有 new 的地方 | 只改容器的装配,调用方零改动 |
| 保证全局唯一连接 | 到处传单例引用 | 容器默认单例,天然只有一个 |
依赖注入不是「花架子」,它是 Java 世界实现 可测试、可扩展、低耦合 的基石。把这个概念吃透,后面所有章节都会顺畅很多。