Bean 作用域与生命周期
Laravel 容器里默认是单例,但也有
bind()(每次新实例)。Spring 的 Bean 默认也是单例,但提供了更丰富的作用域选项。理解单例,就理解了 Java 并发中 90% 的坑。
Bean 的四种作用域
用 @Scope 注解声明:
| 作用域 | 说明 | 类比 Laravel | 默认 |
|---|---|---|---|
singleton | 全局只有一个实例 | singleton() 绑定 | ✅ 默认 |
prototype | 每次注入都新建实例 | bind()(非单例) | |
request | 每个 HTTP 请求一个实例 | 请求级 | |
session | 每个会话一个实例 | 会话级 |
@Component
@Scope("prototype")
public class Calculator { ... }常用的其实只有前两个
Web 应用里 request / session 作用域用得少,因为 Spring MVC 的参数注入已经能拿到请求信息。你只要牢牢记住:默认单例,需要新实例才用 prototype。
单例意味着什么(重点)
@Service
public class UserService {
private int counter = 0; // ❌ 危险的共享状态!
public int hit() {
return ++counter;
}
}UserService 是单例,counter 字段被所有请求线程共享。++counter 在多线程下结果不可预期。
Laravel 的 singleton 也有同样问题,但 PHP 是「每请求隔离」的(一个 PHP-FPM worker 处理完一个请求就销毁),所以没那么容易踩中。Java 是长驻进程 + 多线程,单例 Bean 会被所有并发请求共享,这就是最大的认知差异。
安全的单例 Bean 长什么样
- 无状态:不存可变字段,方法参数进、方法返回出
- 只存配置类依赖:注入其他 Bean、常量、配置
- 确实要存状态 → 用并发安全的容器(
ConcurrentHashMap、数据库、缓存)或改用prototype
@Service
public class UserService {
private final UserRepository repository; // ✅ 只依赖别的 Bean,安全
public UserService(UserRepository repository) {
this.repository = repository;
}
}常见翻车现场
把「数据库连接」存在单例 Bean 里 —— 其实 DataSource 本身就是线程安全的,可以放心;但把「用户信息」存在字段里,就会发生 A 请求的用户信息被 B 请求覆盖。面向无状态设计是 Java 并发的基本功。
Bean 的生命周期钩子
Laravel 服务在容器里创建时可以有 boot;Spring Bean 的生命周期更丰富:
实例化 → 属性注入 → 初始化回调 → 使用中 → 销毁回调初始化回调(@PostConstruct)
对象创建、依赖注入完成后执行一次,类似 Laravel ServiceProvider::boot() 的时机:
import jakarta.annotation.PostConstruct;
@Service
public class StartupTasks {
@PostConstruct
public void init() {
System.out.println("Bean 初始化完成,这里可以预加载数据");
}
}销毁回调(@PreDestroy)
应用关闭时执行一次,适合释放资源:
import jakarta.annotation.PreDestroy;
@Service
public class ResourceHolder {
@PreDestroy
public void cleanup() {
System.out.println("应用关闭,清理资源");
}
}TIP
还有 InitializingBean / DisposableBean 接口和 @Bean(initMethod=...) 两种写法,但 @PostConstruct / @PreDestroy 是最常用、最简洁的。
应用启动时执行一段逻辑:三种方式
这是 Laravel 开发者常问的「Spring 的 boot() 在哪」:
| 方式 | 用法 | 场景 |
|---|---|---|
@PostConstruct | 加在 Bean 方法上 | Bean 初始化后 |
ApplicationRunner | 实现接口 | 整个应用启动完成后执行(最常用) |
CommandLineRunner | 实现接口 | 同 Runner,能拿到命令行参数 |
@Component
public class DataInitializer implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("应用启动完成,初始化数据...");
}
}TIP
对应 Laravel 里「在 ServiceProvider boot 里预加载配置」或 artisan 里的初始化脚本。数据初始化更完整的做法见 数据初始化。
循环依赖
两个 Bean 互相注入对方,就会「先有鸡还是先有蛋」:
A 需要 B,B 需要 A- 构造器注入下,Spring 直接报错,因为无法确定创建顺序
- 字段/setter 注入下,Spring 默认可以(通过提前暴露半成品对象)
最佳实践
遇到循环依赖,说明设计上有问题,应该重构(把公共依赖抽出来),而不是依赖 Spring 的兼容行为。Laravel 里遇到循环 app() 依赖同理。
小结
| 记住这句话 |
|---|
| Bean 默认是单例,所有线程共享 |
| 单例 Bean 要无状态,有状态请用并发安全手段 |
想在启动/销毁时干活,用 @PostConstruct / ApplicationRunner |
| 循环依赖是设计信号,不是框架功能 |