Appearance
Spring Beans 和依赖注入
概述
在 Spring Boot 中,您可以自由使用 Spring Framework 的任何标准技术来定义 Bean 及其注入的依赖项。Spring Boot 推荐使用构造函数注入来连接依赖项,并使用 @ComponentScan
来查找 Bean。
TIP
构造函数注入是 Spring 推荐的依赖注入方式,它可以确保依赖项在对象创建时就被注入,并且可以将字段标记为 final
,提高代码的安全性和不变性。
组件扫描与自动注册
当您按照建议的方式构建代码结构(将应用程序主类放在顶层包中)时,可以添加 @ComponentScan
注解而无需任何参数,或者使用 @SpringBootApplication
注解(它隐式包含了 @ComponentScan
)。
所有应用程序组件(@Component
、@Service
、@Repository
、@Controller
等)都会自动注册为 Spring Bean。
构造函数注入示例
单构造函数注入
当服务类只有一个构造函数时,Spring 会自动使用该构造函数进行依赖注入:
kotlin
import org.springframework.stereotype.Service
/**
* 账户服务实现类
* 使用构造函数注入风险评估器依赖
*/
@Service
class MyAccountService(
// 通过构造函数注入风险评估器
private val riskAssessor: RiskAssessor
) : AccountService {
// 处理账户相关业务逻辑
fun processAccount(account: Account): AccountResult {
// 使用注入的风险评估器进行风险评估
val riskLevel = riskAssessor.assess(account)
return AccountResult(account, riskLevel)
}
}
java
import org.springframework.stereotype.Service;
@Service
public class MyAccountService implements AccountService {
private final RiskAssessor riskAssessor;
public MyAccountService(RiskAssessor riskAssessor) {
this.riskAssessor = riskAssessor;
}
// ...业务逻辑方法
}
NOTE
在 Kotlin 中,主构造函数的参数可以直接声明为类的属性,使用 private val
或 private var
修饰符,这样可以简化代码书写。
多构造函数注入
当 Bean 有多个构造函数时,需要使用 @Autowired
注解标记您希望 Spring 使用的构造函数:
kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.io.PrintStream
/**
* 具有多个构造函数的账户服务
*/
@Service
class MyAccountService : AccountService {
private val riskAssessor: RiskAssessor
private val out: PrintStream
/**
* 主要构造函数 - 使用 @Autowired 标记
* Spring 容器将使用此构造函数进行依赖注入
*/
@Autowired
constructor(riskAssessor: RiskAssessor) {
this.riskAssessor = riskAssessor
this.out = System.out // 默认使用系统输出流
}
/**
* 备用构造函数 - 用于测试或特殊场景
* 允许注入自定义的输出流
*/
constructor(riskAssessor: RiskAssessor, out: PrintStream) {
this.riskAssessor = riskAssessor
this.out = out
}
/**
* 处理账户并输出结果
*/
fun processAccountWithLogging(account: Account): AccountResult {
val riskLevel = riskAssessor.assess(account)
out.println("账户 ${account.id} 的风险等级: $riskLevel")
return AccountResult(account, riskLevel)
}
}
java
import java.io.PrintStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyAccountService implements AccountService {
private final RiskAssessor riskAssessor;
private final PrintStream out;
@Autowired
public MyAccountService(RiskAssessor riskAssessor) {
this.riskAssessor = riskAssessor;
this.out = System.out;
}
public MyAccountService(RiskAssessor riskAssessor, PrintStream out) {
this.riskAssessor = riskAssessor;
this.out = out;
}
// ...业务逻辑方法
}
IMPORTANT
当类有多个构造函数时,必须使用 @Autowired
注解明确指定 Spring 应该使用哪个构造函数进行依赖注入。
实际业务场景示例
让我们通过一个完整的银行账户管理系统来演示依赖注入的实际应用:
定义业务接口和实现
kotlin
// 风险评估器接口
interface RiskAssessor {
fun assess(account: Account): RiskLevel
}
// 账户服务接口
interface AccountService {
fun openAccount(customerInfo: CustomerInfo): Account
fun processTransaction(accountId: String, amount: Double): TransactionResult
}
// 数据访问层接口
interface AccountRepository {
fun save(account: Account): Account
fun findById(id: String): Account?
}
实现具体的业务组件
kotlin
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import org.springframework.stereotype.Repository
/**
* 风险评估器实现 - 使用 @Component 注解
*/
@Component
class DefaultRiskAssessor : RiskAssessor {
override fun assess(account: Account): RiskLevel {
// 基于账户余额和交易历史评估风险
return when {
account.balance > 100000 -> RiskLevel.LOW // 高余额低风险
account.balance > 10000 -> RiskLevel.MEDIUM // 中等余额中等风险
else -> RiskLevel.HIGH // 低余额高风险
}
}
}
/**
* 账户数据访问层实现 - 使用 @Repository 注解
*/
@Repository
class JpaAccountRepository : AccountRepository {
override fun save(account: Account): Account {
// 保存账户到数据库
// 实际实现中会使用JPA EntityManager或Spring Data JPA
println("保存账户: ${account.id}")
return account
}
override fun findById(id: String): Account? {
// 从数据库查找账户
println("查询账户: $id")
return null // 简化示例
}
}
/**
* 完整的账户服务实现 - 使用 @Service 注解
*/
@Service
class BankAccountService(
// 构造函数注入多个依赖
private val riskAssessor: RiskAssessor,
private val accountRepository: AccountRepository
) : AccountService {
override fun openAccount(customerInfo: CustomerInfo): Account {
// 创建新账户
val account = Account(
id = generateAccountId(),
customerId = customerInfo.id,
balance = customerInfo.initialDeposit
)
// 进行风险评估
val riskLevel = riskAssessor.assess(account)
account.riskLevel = riskLevel
// 保存到数据库
return accountRepository.save(account)
}
override fun processTransaction(accountId: String, amount: Double): TransactionResult {
val account = accountRepository.findById(accountId)
?: return TransactionResult.failure("账户不存在")
// 更新余额前重新评估风险
val updatedAccount = account.copy(balance = account.balance + amount)
val newRiskLevel = riskAssessor.assess(updatedAccount)
// 根据风险等级决定是否允许交易
return when (newRiskLevel) {
RiskLevel.HIGH -> {
TransactionResult.failure("风险等级过高,交易被拒绝")
}
else -> {
accountRepository.save(updatedAccount.copy(riskLevel = newRiskLevel))
TransactionResult.success("交易成功")
}
}
}
private fun generateAccountId(): String = "ACC${System.currentTimeMillis()}"
}
应用程序主类
kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
/**
* Spring Boot 应用程序主类
* @SpringBootApplication 包含了 @ComponentScan,会自动扫描并注册所有组件
*/
@SpringBootApplication
class BankingApplication
fun main(args: Array<String>) {
runApplication<BankingApplication>(*args)
}
依赖注入的优势
1. 松耦合
通过依赖注入,类不需要直接创建其依赖对象,而是由容器负责注入,降低了类之间的耦合度。
kotlin
// ❌ 紧耦合的写法
@Service
class BadAccountService : AccountService {
// 直接创建依赖,难以测试和扩展
private val riskAssessor = DefaultRiskAssessor()
private val repository = JpaAccountRepository()
}
// ✅ 松耦合的写法
@Service
class GoodAccountService(
private val riskAssessor: RiskAssessor, // 依赖接口而非具体实现
private val repository: AccountRepository // 便于测试和扩展
) : AccountService
2. 便于测试
使用构造函数注入可以轻松地在单元测试中注入模拟对象:
kotlin
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class BankAccountServiceTest {
@Test
fun `should reject high risk transactions`() {
// 创建模拟对象
val mockRiskAssessor = mock<RiskAssessor>()
val mockRepository = mock<AccountRepository>()
// 通过构造函数注入模拟对象
val service = BankAccountService(mockRiskAssessor, mockRepository)
// 设置模拟行为
whenever(mockRiskAssessor.assess(any())).thenReturn(RiskLevel.HIGH)
// 执行测试
val result = service.processTransaction("ACC123", 1000.0)
// 验证结果
assert(result.isFailure)
assert(result.message.contains("风险等级过高"))
}
}
3. 提高代码安全性
构造函数注入允许将依赖字段标记为 final
(在 Kotlin 中是 val
),确保依赖在对象创建后不会被意外修改:
TIP
使用构造函数注入可以让 riskAssessor
字段被标记为 final
,表明它在后续不能被更改,这提高了代码的不变性和线程安全性。
最佳实践
1. 优先使用构造函数注入
kotlin
// ✅ 推荐:构造函数注入
@Service
class RecommendedService(
private val dependency1: Dependency1,
private val dependency2: Dependency2
) {
// 依赖在构造时注入,不可变且线程安全
}
// ❌ 不推荐:字段注入
@Service
class NotRecommendedService {
@Autowired
private lateinit var dependency1: Dependency1 // 可变,可能存在空指针风险
@Autowired
private lateinit var dependency2: Dependency2
}
2. 合理组织包结构
com.example.banking
├── BankingApplication.kt // 主类
├── controller/ // 控制器层
│ └── AccountController.kt
├── service/ // 服务层
│ ├── AccountService.kt
│ └── impl/
│ └── BankAccountService.kt
├── repository/ // 数据访问层
│ ├── AccountRepository.kt
│ └── impl/
│ └── JpaAccountRepository.kt
└── component/ // 通用组件
└── RiskAssessor.kt
3. 使用接口定义契约
kotlin
// 定义清晰的接口契约
interface PaymentProcessor {
fun processPayment(payment: Payment): PaymentResult
}
// 可以有多个实现
@Component("creditCardProcessor")
class CreditCardProcessor : PaymentProcessor {
override fun processPayment(payment: Payment): PaymentResult {
// 信用卡支付逻辑
}
}
@Component("alipayProcessor")
class AlipayProcessor : PaymentProcessor {
override fun processPayment(payment: Payment): PaymentResult {
// 支付宝支付逻辑
}
}
总结
Spring Boot 的依赖注入机制为我们提供了一种优雅的方式来管理对象之间的依赖关系。通过使用 @ComponentScan
自动扫描和构造函数注入,我们可以构建松耦合、易测试、易维护的应用程序。
IMPORTANT
记住以下关键点:
- 优先使用构造函数注入而非字段注入
- 依赖接口而非具体实现
- 合理使用
@Service
、@Repository
、@Component
等注解 - 当有多个构造函数时,使用
@Autowired
明确指定
TIP
进一步学习
- 了解 Spring 的其他注入方式:setter 注入、字段注入
- 学习
@Qualifier
注解解决多个实现的注入问题 - 探索 Spring Boot 的自动配置机制
- 掌握 Spring 的生命周期回调方法