文档站点
Skip to content

Bean 作用域与生命周期 ​

Laravel 容器里默认是单例,但也有 bind()(每次新实例)。Spring 的 Bean 默认也是单例,但提供了更丰富的作用域选项。理解单例,就理解了 Java 并发中 90% 的坑。

Bean 的四种作用域 ​

用 @Scope 注解声明:

作用域说明类比 Laravel默认
singleton全局只有一个实例singleton() 绑定✅ 默认
prototype每次注入都新建实例bind()(非单例)
request每个 HTTP 请求一个实例请求级
session每个会话一个实例会话级
java
@Component
@Scope("prototype")
public class Calculator { ... }

常用的其实只有前两个

Web 应用里 request / session 作用域用得少,因为 Spring MVC 的参数注入已经能拿到请求信息。你只要牢牢记住:默认单例,需要新实例才用 prototype。

单例意味着什么(重点) ​

java
@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 长什么样 ​

  1. 无状态:不存可变字段,方法参数进、方法返回出
  2. 只存配置类依赖:注入其他 Bean、常量、配置
  3. 确实要存状态 → 用并发安全的容器(ConcurrentHashMap、数据库、缓存)或改用 prototype
java
@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() 的时机:

java
import jakarta.annotation.PostConstruct;

@Service
public class StartupTasks {
    @PostConstruct
    public void init() {
        System.out.println("Bean 初始化完成,这里可以预加载数据");
    }
}

销毁回调(@PreDestroy) ​

应用关闭时执行一次,适合释放资源:

java
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,能拿到命令行参数
java
@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
循环依赖是设计信号,不是框架功能

面向 PHP / Laravel 开发者的 Spring Boot 中文文档