人人都会AI编程

22.4 Bean 设计与作用域选型规范

更新时间:2026-07-11

Bean 的设计并非仅仅写一个类并加上 @Component 那么简单。合理的 Bean 设计与作用域选型,直接决定了应用的性能、并发安全、内存占用和可维护性。本节从实际项目经验出发,归纳一套可落地的规范。

22.4.1 无状态 Bean 优先,有状态 Bean 谨慎

1. 默认设计为无状态服务

绝大多数业务服务、数据访问组件、工具类,都应当是无状态的。所谓无状态,是指 Bean 不持有任何与调用相关的可变数据,所有必要信息都以方法参数形式传入。

@Service
public class OrderCalculationService {
    // 无状态:没有任何实例变量依赖请求上下文
    public BigDecimal calculateTotal(List<OrderItem> items) {
        return items.stream()
                .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQty())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

无状态 Bean 天然具备如下优势:

  • 线程安全:任何线程调用同一实例都不会产生数据串扰。
  • 性能最优:容器可安全地复用同一个单例实例,避免频繁创建对象的开销。
  • 易于测试:方法输入输出明确,无隐藏状态,单元测试无需复杂的环境准备。

2. 有状态 Bean 的使用边界

以下场景需要有状态 Bean,即实例持有与调用上下文关联的数据:

  • 用户会话信息载体(如购物车、登录用户上下文)
  • 请求级别的临时值对象(如请求追踪 ID、计时器等)
  • 需要维护会话生命周期的资源(如某个特定的数据库连接状态机)

此时必须选择合适的作用域,确保并发安全和数据隔离,绝不能用默认的单例。

22.4.2 作用域选型标准

Spring 提供的主要作用域及其适用场景如下:

| 作用域 | 生命周期 | 适用场景 |
|--------|----------|----------|
| singleton(默认) | 整个容器 | 无状态服务、配置对象、线程安全工具、数据源连接池、缓存管理器等全局基础设施。 |
| prototype | 每次注入时创建新实例 | 有状态且需要每次使用的对象都独立的短生命周期组件;也可用于多线程任务中需要独立副本的场景。但需注意容器不管理原型 Bean 的销毁,须自行释放资源。 |
| request | 单个 HTTP 请求 | 请求级别的数据载体,如请求属性、带请求上下文的对象;仅在 Web 环境中可用。 |
| session | 单个 HTTP 会话 | 用户会话相关数据,如购物车、用户资料;注意分布式环境需配合 Spring Session 实现会话外存,以避免单点内存瓶颈。 |
| application | ServletContext 生命周期 | 极少数需要与 Web 应用生命周期绑定的全局对象,与 singleton 类似但作用域不同(单例是 Spring 容器内,可能与 Web 绑定无关)。 |

选型口诀: 能用 singleton 就用 singleton,涉及请求、会话私有数据时才用更窄作用域,prototype 一般仅在明确需要多例时使用。

22.4.3 作用域注入的常见陷阱与解决方案

陷阱一:将短生命周期 Bean 注入到长生命周期单例中

最常见的问题是 prototypesession 作用域 Bean 注入到单例 Bean 中时,注入只发生一次,导致实际使用的仍是第一次创建的那个实例,完全丧失作用域意义。

// 错误示例:单例 Controller 持有同一个 prototype Bean 实例
@RestController
public class ReportController {
    @Autowired
    private ReportGenerator reportGenerator; // prototype 作用域
}

由于 ReportGenerator 只在 ReportController 初始化时注入一次,后续所有请求共享同一个实例,prototype 的“每次新对象”意图直接失效。

正确的解决方案有三种:

方案一:使用 @Lookup 方法注入

@RestController
public class ReportController {
    @Lookup
    public ReportGenerator getReportGenerator() {
        return null; // Spring 会动态代理这个方法,每次调用返回新实例
    }
}

方案二:注入 ObjectFactoryProvider

@RestController
public class ReportController {
    @Autowired
    private ObjectFactory<ReportGenerator> generatorFactory;

    public void handle() {
        ReportGenerator generator = generatorFactory.getObject();
        generator.generate();
    }
}

方案三:使用 @Scope 的代理模式(proxyMode)

在 Bean 定义上声明代理:

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ReportGenerator { ... }

然后直接注入即可,Spring 会注入一个代理,每次方法调用都委托给新创建的实例。注意此方案仅适用于通过代理的注入方式,若通过构造器直接注入代理则仍为单次。

陷阱二:request 作用域在非 Web 线程中使用

request 作用域依赖于当前 HTTP 请求的线程绑定,如果在异步线程、定时任务或消息监听器中尝试访问,会抛出 ScopeNotActiveException。可以使用 RequestContextHolder 传递或改用其他作用域。

陷阱三:prototype Bean 资源未释放

Spring 只负责创建 prototype Bean,不管理其销毁回调。如果原型 Bean 持有文件句柄、网络连接等资源,必须由调用方显式关闭,或者使用 Bean 后处理器进行销毁跟踪。推荐prototype Bean 内尽量避免持有重量级资源。

22.4.4 Bean 设计的工程规范

1. 命名规范与注释

  • 类名清晰反映其职责,如 UserLoginServiceOrderValidator
  • 使用 @Component 自定义 Bean 名称时,遵循小驼峰或具备业务含义的全局标识,不要命名无意义的名字如 bean1
  • 在 Bean 上标注 @Description 或 Javadoc 说明其用途、是否线程安全、持有状态等。

2. 构造函数与不可变性

  • 依赖通过构造器注入且声明为 final,避免使用字段注入。这可以保证 Bean 构造完成后即处于就绪状态,同时方便测试。
  • 避免在构造器中执行复杂初始化逻辑或对外调用(如网络请求),将复杂初始化移至 @PostConstruct 方法,并考虑是否可延迟初始化。

3. 明确状态与线程安全的声明

  • 若一个单例 Bean 出于工程设计考虑而持有可变状态(如缓存 map),务必在类文档中显式说明其线程安全策略(ConcurrentHashMap 或同步控制),并标明状态变更的触发点,避免同事误用。

4. 避免过度使用原型作用域

原型作用域会带来额外的内存压力和 GC 负担,且容器不管理销毁,容易造成资源泄漏。多数“需要新实例”的需求可以通过每次调用时构造一个 POJO(不由容器管理) 来实现,完全不必交给 Spring 容器。

// 推荐方式:直接 new,不纳入容器
public Invoice createInvoice(Order order) {
    return new Invoice(order); 
}

只有在这种 POJO 还需要依赖注入才用原型 Bean,否则容器只应管理单例服务。

5. 条件化 Bean 注册

利用 @Profile@Conditional 对 Bean 进行条件化注册,避免将所有环境 Bean 混在一起。例如开发环境的内存数据库 Bean 和生产环境的数据源 Bean 应严格分开,并通过 profile 激活。

@Configuration
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource devDataSource() { ... }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() { ... }
}

这能显著提升代码的安全性和可读性。

22.4.5 小结

Bean 设计与作用域选型的基本原则可以浓缩为三句话:

  1. 默认无状态单例,只有绝对需要时才缩小作用域。
  2. 短作用域 Bean 注入长作用域 Bean 时,必须通过查找或代理方式在每次使用时获取新实例。
  3. 尽量减少 Spring 容器管理的原型和请求作用域 Bean 的数量,将生命周期管理交给容器本身以降低复杂度。

遵循这些规范,可以使 Bean 设计既满足业务灵活性,又不失工程严密,规避团队协作中常见的作用域陷阱,从而真正发挥 IoC 容器的价值。