Skip to content

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 框架中一个强大而灵活的特性,它让我们能够精确控制对象的生命周期和实例管理策略。通过合理选择和使用作用域,我们可以:

  1. 提高性能:避免不必要的对象创建
  2. 确保正确性:为不同场景提供合适的实例管理
  3. 简化开发:通过声明式配置管理复杂的对象生命周期

IMPORTANT

记住:作用域的选择应该基于业务需求和对象的状态特性。无状态对象优选 singleton,有状态对象根据其生命周期选择合适的作用域。

掌握了 Bean 作用域,你就掌握了 Spring 依赖注入的精髓,能够构建更加健壮和高效的应用程序! 🚀