Appearance
Spring Bean 作用域详解:从单例到自定义的完整指南 🎯
引言:为什么需要 Bean 作用域?
想象一下,你正在开发一个电商系统。有些组件(如数据库连接池)在整个应用中只需要一个实例,而有些组件(如用户购物车)需要为每个用户会话创建独立的实例。这就是 Bean 作用域(Bean Scope) 要解决的核心问题。
IMPORTANT
Bean 作用域决定了 Spring 容器如何创建和管理 Bean 实例的生命周期。它不仅影响内存使用,更关系到应用的正确性和性能。
Bean 作用域概览
Spring Framework 提供了六种作用域,让我们先通过一个表格了解它们:
作用域 | 描述 | 使用场景 |
---|---|---|
singleton | 默认作用域,每个容器只创建一个实例 | 无状态服务、工具类 |
prototype | 每次请求都创建新实例 | 有状态对象、临时对象 |
request | 每个 HTTP 请求创建一个实例 | 请求级别的数据处理 |
session | 每个 HTTP 会话创建一个实例 | 用户会话数据 |
application | 每个 ServletContext 创建一个实例 | 应用级别的配置 |
websocket | 每个 WebSocket 会话创建一个实例 | WebSocket 连接管理 |
1. Singleton 作用域:一个容器一个实例 🏠
核心原理
Singleton 是 Spring 的默认作用域。当你定义一个 singleton bean 时,Spring 容器只会创建该 bean 的一个实例,并将其缓存起来。
实际应用示例
kotlin
@Service // 默认为 singleton 作用域
class UserService(
private val userRepository: UserRepository
) {
// 无状态服务,适合使用 singleton
fun findUserById(id: Long): User? {
return userRepository.findById(id)
}
fun createUser(userDto: UserDto): User {
return userRepository.save(User(userDto.name, userDto.email))
}
}
kotlin
@Configuration
class AppConfig {
@Bean
@Scope("singleton") // 显式指定,虽然这是默认值
fun databaseConfig(): DatabaseConfig {
return DatabaseConfig().apply {
url = "jdbc:mysql://localhost:3306/mydb"
maxConnections = 20
}
}
}
TIP
Singleton 作用域特别适合无状态的服务类,如 Service、Repository、工具类等。它们可以被多个客户端安全地共享。
2. Prototype 作用域:每次请求都是新实例 🔄
核心原理
与 Singleton 相反,Prototype 作用域在每次请求 Bean 时都会创建一个新的实例。
实际应用示例
kotlin
@Component
@Scope("prototype")
class ShoppingCart {
private val items = mutableListOf<CartItem>()
private var userId: Long? = null
fun addItem(product: Product, quantity: Int) {
items.add(CartItem(product, quantity))
}
fun removeItem(productId: Long) {
items.removeIf { it.product.id == productId }
}
fun getTotalPrice(): BigDecimal {
return items.sumOf { it.product.price * it.quantity.toBigDecimal() }
}
// 有状态对象,每个用户需要独立的购物车实例
fun setUser(userId: Long) {
this.userId = userId
}
}
kotlin
@Service
class ShoppingService {
@Autowired
private lateinit var applicationContext: ApplicationContext
fun createNewCart(): ShoppingCart {
// 每次调用都会创建新的 ShoppingCart 实例
return applicationContext.getBean(ShoppingCart::class.java)
}
}
kotlin
@RestController
class CartController(
private val shoppingService: ShoppingService
) {
@PostMapping("/cart/new")
fun createCart(): ResponseEntity<String> {
val cart = shoppingService.createNewCart()
// 每个请求都会得到全新的购物车实例
return ResponseEntity.ok("New cart created")
}
}
WARNING
Prototype Bean 的生命周期管理需要特别注意:Spring 容器不会管理 prototype bean 的销毁,需要客户端代码自行处理资源清理。
3. Web 相关作用域:Request、Session、Application
这些作用域只在 Web 环境中可用,需要配置相应的监听器或过滤器。
初始化 Web 配置
kotlin
class WebAppInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// 注册 RequestContextListener
servletContext.addListener(RequestContextListener::class.java)
}
}
xml
<web-app>
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
</web-app>
Request 作用域
每个 HTTP 请求都会创建一个新的 Bean 实例。
kotlin
@Component
@RequestScope // 等同于 @Scope("request")
class RequestProcessor {
private val startTime = System.currentTimeMillis()
private val requestId = UUID.randomUUID().toString()
fun processRequest(data: String): ProcessResult {
val processingTime = System.currentTimeMillis() - startTime
return ProcessResult(
requestId = requestId,
data = data,
processingTime = processingTime
)
}
fun getRequestId() = requestId
}
Session 作用域
每个 HTTP 会话创建一个 Bean 实例,在整个会话期间保持不变。
kotlin
@Component
@SessionScope // 等同于 @Scope("session")
class UserSession {
private var user: User? = null
private val loginTime = LocalDateTime.now()
private val preferences = mutableMapOf<String, Any>()
fun login(user: User) {
this.user = user
}
fun logout() {
this.user = null
preferences.clear()
}
fun setPreference(key: String, value: Any) {
preferences[key] = value
}
fun isLoggedIn() = user != null
}
Application 作用域
在整个 ServletContext 生命周期内只创建一个实例。
kotlin
@Component
@ApplicationScope // 等同于 @Scope("application")
class ApplicationConfig {
private val configMap = mutableMapOf<String, String>()
private val startupTime = LocalDateTime.now()
@PostConstruct
fun loadConfig() {
// 加载应用级别的配置
configMap["version"] = "1.0.0"
configMap["environment"] = "production"
}
fun getConfig(key: String) = configMap[key]
fun getStartupTime() = startupTime
}
4. 作用域依赖注入的挑战与解决方案
问题场景
当一个长生命周期的 Bean(如 singleton)依赖一个短生命周期的 Bean(如 request)时,会出现问题:
kotlin
@Service // singleton 作用域
class OrderService(
private val requestProcessor: RequestProcessor // request 作用域
) {
// ❌ 问题:singleton bean 只会在启动时注入一次 request bean
// 后续所有请求都会使用同一个 RequestProcessor 实例
fun processOrder(order: Order) {
requestProcessor.processRequest(order.toString())
}
}
解决方案:作用域代理
kotlin
@Component
@RequestScope
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
class RequestProcessor {
private val startTime = System.currentTimeMillis()
private val requestId = UUID.randomUUID().toString()
fun processRequest(data: String): ProcessResult {
val processingTime = System.currentTimeMillis() - startTime
return ProcessResult(
requestId = requestId,
data = data,
processingTime = processingTime
)
}
}
代理工作原理
替代方案:使用 ObjectProvider
kotlin
@Service
class OrderService(
private val requestProcessorProvider: ObjectProvider<RequestProcessor>
) {
fun processOrder(order: Order) {
// 每次调用时获取当前请求作用域的实例
val processor = requestProcessorProvider.getObject()
processor.processRequest(order.toString())
}
}
5. 自定义作用域:打造专属的生命周期管理
创建自定义作用域
假设我们需要一个"线程作用域",在同一个线程中共享 Bean 实例:
kotlin
class ThreadScope : Scope {
private val threadLocal = ThreadLocal<MutableMap<String, Any>>()
override fun get(name: String, objectFactory: ObjectFactory<*>): Any {
val scope = threadLocal.get() ?: run {
val newScope = mutableMapOf<String, Any>()
threadLocal.set(newScope)
newScope
}
return scope.getOrPut(name) {
objectFactory.getObject()
}
}
override fun remove(name: String): Any? {
val scope = threadLocal.get() ?: return null
return scope.remove(name)
}
override fun registerDestructionCallback(name: String, callback: Runnable) {
// 注册销毁回调
}
override fun resolveContextualObject(key: String): Any? {
return null
}
override fun getConversationId(): String? {
return Thread.currentThread().name
}
}
注册自定义作用域
kotlin
@Configuration
class ScopeConfig {
@Bean
fun customScopeConfigurer(): CustomScopeConfigurer {
val configurer = CustomScopeConfigurer()
configurer.setScopes(mapOf("thread" to ThreadScope()))
return configurer
}
}
使用自定义作用域
kotlin
@Component
@Scope("thread")
class ThreadLocalCache {
private val cache = mutableMapOf<String, Any>()
fun put(key: String, value: Any) {
cache[key] = value
}
fun get(key: String): Any? = cache[key]
fun clear() = cache.clear()
}
6. 最佳实践与注意事项
选择合适的作用域
作用域选择指南
- Singleton:无状态服务、工具类、配置类
- Prototype:有状态对象、需要独立实例的组件
- Request:请求级别的数据处理、临时计算
- Session:用户会话数据、购物车、用户偏好
- Application:应用级别的配置、缓存
性能考虑
kotlin
// ✅ 推荐:对于无状态服务使用 singleton
@Service
class UserService {
fun findUser(id: Long): User? {
// 无状态操作,适合 singleton
return userRepository.findById(id)
}
}
// ❌ 避免:不必要的 prototype 会影响性能
@Service
@Scope("prototype")
class StatelessCalculator {
fun calculate(a: Int, b: Int): Int {
return a + b // 无状态计算,不需要 prototype
}
}
内存泄漏预防
kotlin
@Component
@Scope("prototype")
class ResourceHolder : DisposableBean {
private val resources = mutableListOf<Closeable>()
fun addResource(resource: Closeable) {
resources.add(resource)
}
// ✅ 实现销毁逻辑,防止内存泄漏
override fun destroy() {
resources.forEach { resource ->
try {
resource.close()
} catch (e: Exception) {
// 记录日志但不抛出异常
}
}
resources.clear()
}
}
总结
Bean 作用域是 Spring 框架中一个强大而灵活的特性,它让我们能够精确控制对象的生命周期和实例管理策略。通过合理选择和使用作用域,我们可以:
- 提高性能:避免不必要的对象创建
- 确保正确性:为不同场景提供合适的实例管理
- 简化开发:通过声明式配置管理复杂的对象生命周期
IMPORTANT
记住:作用域的选择应该基于业务需求和对象的状态特性。无状态对象优选 singleton,有状态对象根据其生命周期选择合适的作用域。
掌握了 Bean 作用域,你就掌握了 Spring 依赖注入的精髓,能够构建更加健壮和高效的应用程序! 🚀