Spring 的核心是圍繞 Bean 進行的。不管是 Spring Boot 還是 Spring Cloud,只要名稱中帶有 Spring 關鍵字的技術都脫離不了 Bean。


IoC 容器原理#

IoC(Inversion of Control,控制反轉)是 Spring 的核心概念,透過將物件的建立與管理交由容器負責,實現鬆耦合的設計。

核心元件#

元件職責
BeanFactoryIoC 容器的基礎介面
ApplicationContextBeanFactory 的擴展,提供更多企業級功能
BeanDefinitionBean 的後設資料定義
BeanPostProcessorBean 後處理器,用於擴展 Bean 初始化邏輯

容器啟動流程#

flowchart LR
    A[載入組態] --> B[解析 BeanDefinition]
    B --> C[實體化 Bean]
    C --> D[屬性注入]
    D --> E[初始化]
    E --> F[就緒]

Bean 的生命週期#

Bean 生命週期三大關鍵步驟

  1. createBeanInstance - 通過構造器反射建立實體
  2. populateBean - 填充(注入)Bean 的依賴
  3. 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
來源SpringJSR-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 的 Bean

Bean 命名規則

  • 一般類別:首字母小寫(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:#ffcccc
package 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 -->|依賴| A

Spring 如何解決#

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

以下情況無法解決循環依賴:

  1. 構造器注入的循環依賴
  2. Prototype 作用域的循環依賴
  3. @Async 等需要代理的 Bean 可能出問題

最佳實踐#

// 避免循環依賴的方法:
// 1. 重新設計,打破循環
// 2. 使用 @Lazy 延遲載入
@Autowired
@Lazy
private ServiceB serviceB;

常見錯誤速查#

錯誤原因解決方案
Bean 找不到不在掃描範圍內檢查 @ComponentScan
構造器 NPE在構造器中使用未注入的成員改用 @PostConstruct
多 Bean 無法選擇同類型 Bean 有多個用 @Primary 或 @Qualifier
Prototype 不生效被 Singleton 注入後固定用 @Lookup 或 ApplicationContext
循環依賴報錯構造器注入形成循環改用 Setter 注入或 @Lazy