在Spring框架中,Bean的作用域(Scope)决定了容器如何创建和管理Bean实例。单例(Singleton)和多例(Prototype)是两种最基础也最容易混淆的作用域类型。理解它们的底层机制,对于设计合理的Spring应用架构至关重要。
单例是Spring默认的作用域模式。当我们将一个Bean定义为单例时,意味着:
java复制// 典型单例Bean定义方式
@Component
@Scope("singleton") // 可省略,默认即为singleton
public class SingletonService {
// 类实现
}
多例作用域则表现出完全不同的行为模式:
java复制// 多例Bean定义方式
@Component
@Scope("prototype")
public class PrototypeService {
// 类实现
}
重要提示:多例Bean的@PreDestroy方法不会被执行,因为Spring容器不跟踪这些实例的生命周期
当请求一个prototype作用域的Bean时,Spring容器会执行以下步骤:
java复制// AbstractBeanFactory.doGetBean()关键代码片段
if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
return prototypeInstance;
}
| 特性 | 单例(Singleton) | 多例(Prototype) |
|---|---|---|
| 实例数量 | 每个容器一个 | 每次请求新实例 |
| 缓存机制 | 使用三级缓存 | 无缓存 |
| 初始化时机 | 容器启动时(默认) | 每次请求时 |
| 销毁回调 | 支持@PreDestroy | 不支持 |
| 性能 | 高(复用实例) | 低(频繁创建) |
| 适用场景 | 无状态服务 | 有状态对象 |
最常见的误区是在单例Bean中直接注入多例Bean:
java复制@Service
public class OrderService { // 单例
@Autowired
private PaymentService paymentService; // 多例
public void processOrder() {
paymentService.charge(); // 每次调用都是同一个paymentService实例
}
}
这种现象的产生是因为:
java复制@Service
public abstract class OrderService {
@Lookup
protected abstract PaymentService createPaymentService();
public void processOrder() {
PaymentService payment = createPaymentService(); // 每次都是新实例
payment.charge();
}
}
实现原理:
java复制@Service
public class OrderService {
@Autowired
private ObjectProvider<PaymentService> paymentServiceProvider;
public void processOrder() {
PaymentService payment = paymentServiceProvider.getObject(); // 按需获取新实例
payment.charge();
}
}
优势:
java复制@Service
public class OrderService {
@Lazy
@Autowired
private PaymentService paymentService;
public void processOrder() {
paymentService.charge(); // 每次调用都会通过代理获取新实例
}
}
实现机制:
java复制@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void processOrder() {
PaymentService payment = context.getBean(PaymentService.class);
payment.charge();
}
}
不推荐原因:
多例Bean特别适合有状态的服务场景,例如:
java复制@Scope("prototype")
public class UserSession {
private String userId;
private LocalDateTime loginTime;
// 有状态的业务方法
public void refreshSession() {
this.loginTime = LocalDateTime.now();
}
}
虽然多例Bean每次都是新实例,但仍需注意:
频繁创建多例Bean可能带来性能开销,可以考虑:
java复制public class PrototypePool implements ObjectPool<ExpensiveBean> {
private final List<ExpensiveBean> pool = Collections.synchronizedList(new ArrayList<>());
@Override
public ExpensiveBean borrowObject() {
if(pool.isEmpty()) {
return new ExpensiveBean();
}
return pool.remove(0);
}
@Override
public void returnObject(ExpensiveBean obj) {
pool.add(obj);
}
}
现象:每次获取的都是同一个实例
排查步骤:
风险点:
解决方案:
优化建议:
java复制@Scope("prototype")
public class HeavyResourceBean {
@PostConstruct
public void init() {
// 将耗时操作放在这里而非构造函数
loadHeavyResources();
}
}
Spring的多例作用域本质上是原型模式(Prototype Pattern)的实现:
| 考虑因素 | 选择单例 | 选择多例 |
|---|---|---|
| 对象状态 | 无状态 | 有状态 |
| 线程安全 | 需要保证 | 不关键 |
| 创建成本 | 高 | 低 |
| 使用频率 | 高 | 低 |
| 资源占用 | 大 | 小 |
在DDD中,多例作用域适合:
java复制@Scope("prototype")
public class OrderFactory {
public Order createNewOrder(Customer customer) {
Order order = new Order();
order.setCustomer(customer);
order.setStatus(OrderStatus.NEW);
return order;
}
}
在实际项目开发中,我通常会根据业务场景灵活组合各种作用域。对于核心服务类保持单例,对于有状态的处理器或上下文对象使用多例,并通过ObjectProvider进行按需获取。这种组合方式既能保证系统性能,又能满足复杂业务场景的需求。