一、核心概念
1. Bean 的作用域(Scope)
Spring 容器中 Bean 的生命周期范围和可见性。Spring Boot 支持以下作用域:
作用域 | 说明 | 使用场景 |
---|---|---|
singleton (默认) |
每个 Spring IoC 容器中仅存在一个共享的 Bean 实例 | 大多数无状态组件(Service, Repository) |
prototype |
每次请求(getBean() 或注入)都创建一个新的 Bean 实例 |
有状态的、需要独立状态的对象 |
request |
每个 HTTP 请求创建一个新实例,仅在 Web 环境有效 | 与单个 HTTP 请求相关的数据 |
session |
每个 HTTP Session 创建一个新实例,仅在 Web 环境有效 | 用户会话数据 |
application |
为整个 ServletContext 生命周期创建一个 Bean,仅在 Web 环境有效 |
应用级共享数据 |
websocket |
每个 WebSocket 会话创建一个 Bean,仅在 WebSocket 环境有效 | WebSocket 会话数据 |
2. Bean 的生命周期
指 Bean 从创建、初始化、使用到销毁的全过程。Spring 提供了多个扩展点:
- 实例化 (Instantiation)
- 属性赋值 (Populate Properties)
- 初始化前 (BeanPostProcessor.postProcessBeforeInitialization)
- 初始化 (InitializingBean.afterPropertiesSet / @PostConstruct / init-method)
- 初始化后 (BeanPostProcessor.postProcessAfterInitialization)
- 使用 (In Use)
- 销毁前 (DestructionAwareBeanPostProcessor.postProcessBeforeDestruction)
- 销毁 (DisposableBean.destroy / @PreDestroy / destroy-method)
二、操作步骤(非常详细)
步骤 1:定义不同作用域的 Bean
1.1 Singleton Bean (默认)
@Component // 默认 scope = singleton
public class UserService {
private int callCount = 0;
public void doSomething() {
System.out.println("UserService called: " + ++callCount);
}
}
1.2 Prototype Bean
@Component
@Scope("prototype") // 或 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TransactionContext {
private String transactionId;
private LocalDateTime createTime;
public TransactionContext() {
this.transactionId = UUID.randomUUID().toString();
this.createTime = LocalDateTime.now();
System.out.println("TransactionContext created: " + transactionId);
}
// getters and setters
}
1.3 Request Scope Bean
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private Instant requestTime;
public RequestContext() {
this.requestId = UUID.randomUUID().toString();
this.requestTime = Instant.now();
System.out.println("RequestContext created for request: " + requestId);
}
// getters and setters
}
proxyMode = TARGET_CLASS
:创建 CGLIB 代理,解决 singleton Bean 注入 request scope Bean 的问题。
1.4 Session Scope Bean
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
private String userId;
private Map<String, Object> attributes = new HashMap<>();
// getters, setters, and methods
}
步骤 2:在控制器中使用不同作用域的 Bean
@RestController
@RequestMapping("/scope")
public class ScopeController {
@Autowired
private UserService userService; // Singleton
@Autowired
private ApplicationContext applicationContext; // 用于获取 Prototype Bean
@Autowired
private RequestContext requestContext; // Request Scoped (via proxy)
@Autowired
private UserSession userSession; // Session Scoped (via proxy)
@GetMapping("/singleton")
public String testSingleton() {
userService.doSomething(); // 调用次数会累积
return "UserService call count: " + getCallCountFromUserService();
}
@GetMapping("/prototype")
public String testPrototype() {
// 必须通过 ApplicationContext 获取新实例
TransactionContext ctx1 = applicationContext.getBean(TransactionContext.class);
TransactionContext ctx2 = applicationContext.getBean(TransactionContext.class);
return "Ctx1 ID: " + ctx1.getTransactionId() + ", Ctx2 ID: " + ctx2.getTransactionId() +
", Are they same? " + (ctx1 == ctx2);
}
@GetMapping("/request")
public String testRequest() {
return "Current Request ID: " + requestContext.getRequestId() +
", Time: " + requestContext.getRequestTime();
}
@PostMapping("/session/user")
public String setUserInSession(@RequestParam String userId) {
userSession.setUserId(userId);
return "User " + userId + " set in session.";
}
@GetMapping("/session/user")
public String getUserFromSession() {
return "Current User in Session: " + userSession.getUserId();
}
}
步骤 3:定义 Bean 生命周期回调
3.1 使用 @PostConstruct
和 @PreDestroy
@Component
public class DatabaseConnectionManager {
@PostConstruct
public void init() {
System.out.println("DatabaseConnectionManager: Initializing connections...");
// 建立数据库连接池等
}
@PreDestroy
public void cleanup() {
System.out.println("DatabaseConnectionManager: Cleaning up connections...");
// 关闭连接池
}
}
3.2 实现 InitializingBean
和 DisposableBean
@Component
public class FileWatcherService implements InitializingBean, DisposableBean {
private WatchService watchService;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("FileWatcherService: Starting file watcher...");
watchService = FileSystems.getDefault().newWatchService();
// 开始监听文件
}
@Override
public void destroy() throws Exception {
System.out.println("FileWatcherService: Stopping file watcher...");
if (watchService != null) {
watchService.close();
}
}
}
3.3 使用 XML 配置的 init-method
和 destroy-method
(较少用)
public class LegacyService {
public void startUp() {
System.out.println("LegacyService: Starting up...");
}
public void shutDown() {
System.out.println("LegacyService: Shutting down...");
}
}
<bean id="legacyService" class="com.example.LegacyService"
init-method="startUp" destroy-method="shutDown"/>
步骤 4:使用 BeanPostProcessor
(高级)
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Before Initialization: " + beanName);
// 可以修改 bean 或返回代理
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("After Initialization: " + beanName);
return bean;
}
}
三、常见错误与解决方案
错误 | 原因 | 解决方案 |
---|---|---|
BeanCreationException : Scope 'request' is not active |
在非 Web 请求上下文中访问 request scope Bean | 确保在 Web 请求中访问,或使用 @Lazy 延迟加载 |
Singleton Bean 注入 Prototype Bean 失效 | Singleton Bean 在初始化时只获取一次 Prototype 实例 | 使用 @Scope(proxyMode = ...) 或通过 ApplicationContext 获取 |
@PreDestroy 方法未执行 |
应用正常关闭时未调用 context.close() |
确保 ApplicationContext 被正确关闭(如 SpringApplication.exit() ) |
循环依赖导致初始化失败 | A 依赖 B,B 依赖 A | 使用 @Lazy 、@PostConstruct 中注入、或重构设计 |
InitializingBean.afterPropertiesSet 抛出异常 |
初始化逻辑失败 | 捕获并处理异常,或使用 @PostConstruct 提供更清晰的错误信息 |
四、注意事项
prototype
Bean 的销毁:Spring 容器不管理prototype
Bean 的销毁,需要客户端代码负责清理资源。- 作用域代理 (
proxyMode
):当singleton
Bean 需要注入request
/session
等 Web 作用域 Bean 时,必须使用proxyMode
,否则注入的是代理对象创建时的那个实例。 @PostConstruct
/@PreDestroy
与 JSR-250:需要jakarta.annotation-api
依赖(Spring Boot 通常已包含)。- 生命周期方法执行顺序:
@PostConstruct
->InitializingBean.afterPropertiesSet
->init-method
destroy-method
->DisposableBean.destroy
->@PreDestroy
BeanPostProcessor
影响所有 Bean:谨慎实现,避免性能问题或意外行为。
五、使用技巧
@Lazy
注解:- 延迟单个 Bean 的初始化:
@Component @Lazy
- 延迟依赖注入:
@Autowired @Lazy private ExpensiveService service;
- 全局延迟:
@SpringBootApplication @EnableLazyInitialization
- 延迟单个 Bean 的初始化:
ObjectProvider
:安全地获取可能不存在或需要按需获取的 Bean。
@Autowired
private ObjectProvider<OptionalFeature> optionalFeatureProvider;
public void useOptionalFeature() {
OptionalFeature feature = optionalFeatureProvider.getIfAvailable();
if (feature != null) {
feature.doSomething();
}
}
@Lookup
方法注入:解决 singleton Bean 获取 prototype Bean 的另一种方式(较少用)。
@Component
public abstract class CommandManager {
public void processCommand() {
// 每次调用createCommand()都返回一个新的Command实例
Command command = createCommand();
command.execute();
}
@Lookup // Spring 会动态生成子类实现此方法
protected abstract Command createCommand();
}
@Scope
与@Configuration
类:在@Configuration
类中使用@Bean
定义时,@Scope
直接标注在方法上。
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public MyPrototypeBean myPrototypeBean() {
return new MyPrototypeBean();
}
}
六、最佳实践
- 默认使用
singleton
:绝大多数无状态服务、数据访问对象都应是单例。 - 谨慎使用
prototype
:仅在对象有独立状态且状态不共享时使用,注意资源清理。 - Web 作用域明确:
request
用于请求上下文,session
用于用户会话数据,避免滥用。 - 优先使用
@PostConstruct
/@PreDestroy
:比实现接口更简洁,是首选方式。 - 避免在
@PostConstruct
中做耗时操作:会阻塞应用启动。考虑使用@EventListener
监听ContextRefreshedEvent
。 - 清晰的资源管理:对于需要清理的资源(文件句柄、网络连接),务必在
@PreDestroy
或DisposableBean.destroy
中释放。 - 使用
@Profile
隔离环境相关 Bean:避免在错误环境中创建 Bean。
七、性能优化
- 减少
prototype
Bean 的创建:频繁创建和销毁对象有开销。考虑使用对象池(如 Commons Pool2)。 - 延迟初始化 (
@Lazy
):减少启动时间,仅在需要时初始化大型或耗时 Bean。 - 避免在
BeanPostProcessor
中执行重操作:它会影响所有 Bean 的创建。 - 合理使用作用域代理:CGLIB 代理有轻微性能开销,仅在必要时使用
proxyMode
。 - 监控 Bean 生命周期:利用日志或 APM 工具监控
@PostConstruct
和@PreDestroy
的执行时间和频率,识别瓶颈。
总结
掌握 Bean 的作用域是理解对象生命周期和依赖关系的基础,而掌握生命周期是实现资源管理、初始化逻辑和优雅关闭的关键。
快速掌握路径:
- 动手实践:创建不同作用域的 Bean 并在控制器中测试。
- 观察日志:在
@PostConstruct
和@PreDestroy
中添加日志,观察启动和关闭过程。 - 使用
--debug
:查看应用启动时的 Bean 创建报告。 - 阅读官方文档:深入理解
BeanPostProcessor
等高级特性。
理解并正确应用这些概念,是构建健壮、高效 Spring Boot 应用的基石。