Spring 的核心是圍繞 Bean 進行的。不管是 Spring Boot 還是 Spring Cloud,只要名稱中帶有 Spring 關鍵字的技術都脫離不了 Bean。
IoC 容器原理#
IoC(Inversion of Control,控制反轉)是 Spring 的核心概念,透過將物件的建立與管理交由容器負責,實現鬆耦合的設計。
核心元件#
| 元件 | 職責 |
|---|---|
BeanFactory | IoC 容器的基礎介面 |
ApplicationContext | BeanFactory 的擴展,提供更多企業級功能 |
BeanDefinition | Bean 的後設資料定義 |
BeanPostProcessor | Bean 後處理器,用於擴展 Bean 初始化邏輯 |
容器啟動流程#
flowchart LR
A[載入組態] --> B[解析 BeanDefinition]
B --> C[實體化 Bean]
C --> D[屬性注入]
D --> E[初始化]
E --> F[就緒]Bean 的生命週期#
Bean 生命週期三大關鍵步驟
- createBeanInstance - 通過構造器反射建立實體
- populateBean - 填充(注入)Bean 的依賴
- initializeBean - 執行初始化回呼(如
@PostConstruct)
生命週期回呼順序#
// 1. 構造器
public MyBean() { }
// 2. 依賴注入完成後
@PostConstruct
public void init() { }
// 或實作 InitializingBean
@Override
public void afterPropertiesSet() { }
// 3. 容器關閉時
@PreDestroy
public void cleanup() { }sequenceDiagram
participant 容器
participant Bean
容器->>Bean: 1. 呼叫構造器
容器->>Bean: 2. 依賴注入 (populateBean)
容器->>Bean: 3. @PostConstruct
容器->>Bean: 4. InitializingBean.afterPropertiesSet()
容器->>Bean: 5. 自訂 init-method
Note over Bean: Bean 就緒,開始服務
容器->>Bean: 6. @PreDestroy
容器->>Bean: 7. DisposableBean.destroy()初始化方法的執行時機#
initializeBean 方法內部的關鍵執行:
protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
// 1. 執行 @PostConstruct 標記的方法
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
// 2. 執行 InitializingBean.afterPropertiesSet()
invokeInitMethods(beanName, wrappedBean, mbd);
// 3. 執行自訂 init-method
// ...
}構造器中不能使用 @Autowired 注入的成員
因為
populateBean(負責注入)是在createBeanInstance(呼叫構造器)之後執行的!
案例:構造器中使用注入成員導致 NPE
錯誤寫法:
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
public LightMgrService() {
// 此時 lightService 還是 null!
lightService.check(); // NullPointerException
}
}正確寫法 1:使用構造器注入
@Component
public class LightMgrService {
private LightService lightService;
public LightMgrService(LightService lightService) {
this.lightService = lightService;
lightService.check(); // 正常工作
}
}正確寫法 2:使用 @PostConstruct
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
@PostConstruct
public void init() {
lightService.check(); // 正常工作
}
}依賴注入方式#
三種注入方式比較#
| 方式 | 優點 | 缺點 |
|---|---|---|
| 構造器注入 | 不可變、強依賴明確、便於測試 | 參數過多時不美觀 |
| Setter 注入 | 可選依賴、可重新組態 | 物件可能處於不完整狀態 |
| Field 注入 | 簡潔 | 難以測試、隱藏依賴關係 |
最佳實踐:優先使用構造器注入
Spring 官方推薦構造器注入,因為它能確保依賴的不可變性,且在測試時更容易 mock。
@Autowired vs @Resource#
| 特性 | @Autowired | @Resource |
|---|---|---|
| 來源 | Spring | JSR-250 |
| 預設匹配方式 | 按類型 | 按名稱 |
| required 屬性 | 有 | 無 |
多個候選 Bean 的處理#
當存在多個相同類型的 Bean 時,Spring 按以下順序決定注入哪個:
flowchart TD
A[發現多個候選 Bean] --> B{有 @Primary?}
B -->|是| C[注入 @Primary 標記的 Bean]
B -->|否| D{有 @Qualifier?}
D -->|是| E[注入指定名稱的 Bean]
D -->|否| F{變數名匹配 Bean 名?}
F -->|是| G[注入同名 Bean]
F -->|否| H[拋出 NoUniqueBeanDefinitionException]// 方法 1:使用 @Primary 標記優先
@Repository
@Primary
public class OracleDataService implements DataService { }
// 方法 2:使用 @Qualifier 明確指定
@Autowired
@Qualifier("oracleDataService")
private DataService dataService;
// 方法 3:變數名稱與 Bean 名稱一致
@Autowired
private DataService oracleDataService; // 自動匹配名為 oracleDataService 的 BeanBean 命名規則
- 一般類別:首字母小寫(
MyService->myService)- 連續大寫開頭:保持原樣(
SQLiteService->SQLiteService)
Bean 作用域#
| 作用域 | 說明 |
|---|---|
singleton | 預設,整個容器只有一個實體 |
prototype | 每次請求建立新實體 |
request | 每個 HTTP 請求一個實體 |
session | 每個 HTTP Session 一個實體 |
原型 Bean 被固定的問題#
Singleton 注入 Prototype 會導致 Prototype 失效
flowchart LR
subgraph 問題
direction TB
S1[Singleton Controller] -->|注入一次| P1[Prototype Bean]
S1 -.->|後續請求| P1
end
subgraph 解決方案
direction TB
S2[Singleton Controller] -->|每次呼叫| AC[ApplicationContext.getBean]
AC -->|每次新建| P2[Prototype Bean 1]
AC -->|每次新建| P3[Prototype Bean 2]
end案例:原型 Bean 被固定
問題程式碼:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl { }
@RestController
public class HelloWorldController {
@Autowired
private ServiceImpl serviceImpl; // 只會注入一次,之後固定不變
}解決方案 1:注入 ApplicationContext
@RestController
public class HelloWorldController {
@Autowired
private ApplicationContext applicationContext;
public ServiceImpl getServiceImpl() {
return applicationContext.getBean(ServiceImpl.class);
}
}解決方案 2:使用 @Lookup
@RestController
public class HelloWorldController {
@Lookup
public ServiceImpl getServiceImpl() {
return null; // 實際由 Spring CGLIB 代理實現
}
}元件掃描規則#
@ComponentScan 預設範圍#
@SpringBootApplication預設只掃描啟動類所在套件及其子套件
flowchart TD
subgraph 會被掃描
A[com.example.application]
A --> B[com.example.application.service]
A --> C[com.example.application.controller]
end
subgraph 不會被掃描
D[com.example.controller]
E[com.example.service]
end
style D fill:#ffcccc
style E fill:#ffccccpackage com.example.application;
@SpringBootApplication // 只掃描 com.example.application.*
public class Application { }如果 Controller 在 com.example.controller 套件下,將不會被掃描到!
解決方案:
@SpringBootApplication
@ComponentScan("com.example") // 明確指定掃描範圍
public class Application { }循環依賴問題#
什麼是循環依賴#
flowchart LR
A[Bean A] -->|依賴| B[Bean B]
B -->|依賴| C[Bean C]
C -->|依賴| ASpring 如何解決#
Spring 通過三級快取解決 Singleton Bean 的循環依賴:
flowchart TB
subgraph 三級快取
L1[一級快取<br>singletonObjects<br>完全初始化的 Bean]
L2[二級快取<br>earlySingletonObjects<br>提前曝光的 Bean]
L3[三級快取<br>singletonFactories<br>Bean 工廠]
end
A[建立 Bean A] --> L3
L3 -->|A 需要 B| B[建立 Bean B]
B -->|B 需要 A| L3
L3 -->|取得 A 的早期參照| L2
L2 -->|注入給 B| B
B -->|B 完成初始化| L1
L1 -->|注入給 A| A
A -->|A 完成初始化| L1以下情況無法解決循環依賴:
- 構造器注入的循環依賴
- Prototype 作用域的循環依賴
@Async等需要代理的 Bean 可能出問題
最佳實踐#
// 避免循環依賴的方法:
// 1. 重新設計,打破循環
// 2. 使用 @Lazy 延遲載入
@Autowired
@Lazy
private ServiceB serviceB;常見錯誤速查#
| 錯誤 | 原因 | 解決方案 |
|---|---|---|
| Bean 找不到 | 不在掃描範圍內 | 檢查 @ComponentScan |
| 構造器 NPE | 在構造器中使用未注入的成員 | 改用 @PostConstruct |
| 多 Bean 無法選擇 | 同類型 Bean 有多個 | 用 @Primary 或 @Qualifier |
| Prototype 不生效 | 被 Singleton 注入後固定 | 用 @Lookup 或 ApplicationContext |
| 循環依賴報錯 | 構造器注入形成循環 | 改用 Setter 注入或 @Lazy |