iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Software Development

spring boot 3 學習筆記系列 第 10

Day10 - Spring Boot 依賴注入入門

  • 分享至 

  • xImage
  •  

前言

在 Spring Boot 中,有一個非常核心的觀念就是 依賴注入 (Dependency Injection, DI)。它與 控制反轉 (Inversion of Control, IoC) 密切相關,能幫助我們降低程式碼的耦合度、提升可維護性與可測試性。本節會透過生活化的比喻(珍珠奶茶、咖啡機)、程式碼比較、圖解.. 等方式快速理解與掌握。

常見專有名詞小詞典

名詞 英文 簡單解釋
Bean Bean Spring 容器所管理的物件
IoC Inversion of Control 將建立物件的責任交給 Spring 容器
DI Dependency Injection Spring 容器將物件「注入」到需要它的地方

什麼是 Bean?

在 Spring Boot 中,Bean 是由 Spring 控制反轉 (IoC) 容器建立並管理的 Java 物件。當應用程式啟動時,Spring 會自動建立、設定並管理這些物件,讓開發者專注於業務邏輯,而不需要自己處理物件的生命週期。

為什麼需要 Bean?

  • 依賴注入 (DI):Spring 可以自動將一個 Bean 注入到另一個 Bean 中。
  • 生命週期管理:Spring 負責建立和銷毀 Bean。
  • 設定管理:可以透過註解 (Annotation) 或設定檔來設定 Bean 的行為。

依賴 (Dependency):從珍珠奶茶開始

在程式中,一個類別 (Class) 經常需要另一個類別幫忙才能完成工作,這就是「依賴 (Dependency)」。

生活比喻:

  • 你是一個「顧客 (Customer)」,想喝「珍珠奶茶 (BubbleTea)」。
  • 你必須依賴「飲料店 (TeaShop)」來製作。

如果用程式中來表示,在沒有 DI 的世界裡,你可能會這樣做:

// 飲料店類別
public class TeaShop {
    public String makeBubbleTea() {
        return "一杯香濃的珍珠奶茶";
    }
}

// 顧客類別
public class Customer {
    private TeaShop teaShop;

    public Customer() {
        // **自己動手建立依賴,控制權在自己手上**,建立一個飲料店物件
        this.teaShop = new TeaShop();
    }

    public void drink() {
        String bubbleTea = teaShop.makeBubbleTea();
        System.out.println("顧客喝到了:" + bubbleTea);
    }
}

// 主程式
public class Main {
    public static void main(String[] args) {
        Customer customer = new Customer();
        customer.drink();
    }
}

問題在哪?

  • 可以發現 Customer 類別自己負責 new TeaShop()。這代表 CustomerTeaShop 緊緊綁在一起這種情況我們稱為「高耦合 (High Coupling)」。
  • 如果今天你想要換成「街角飲料店 (CornerTeaShop)」,就必須修改 Customer 內部的程式碼。如果是在大型專案中,這樣的修改會像惡夢一樣,牽一髮而動全身。

控制反轉 (IoC):專業的事交給專業的來

為了解決這個問題,Spring 提出了 控制反轉 (Inversion of Control, IoC) 的概念。

概念

  • 你 (顧客) 不需要自己開店,只需要告訴 Spring:「我需要一杯珍奶」。
  • Spring 容器會自動建立 TeaShop,並把它交給你。

這個「把依賴交給你」的動作,就是 依賴注入 (Dependency Injection, DI)

IoC 流程圖

[Spring IoC 容器]
     │
     ├── 1. 建立 TeaShop Bean
     ├── 2. 建立 CustomerService Bean
     └── 3. 將 TeaShop Bean「注入」到 CustomerService 中

Spring Boot 中的三種依賴注入方式

首先,我們要讓 Spring 知道 TeaShop 是需要被它管理的 Bean。

import org.springframework.stereotype.Service;

@Service // 告訴 Spring:這是一個需要被你管理的 Bean
public class TeaShop {
    public String makeBubbleTea() {
        return "一杯來自 Spring 容器的珍珠奶茶";
    }
}

接下來,看看 CustomerService 如何取得 TeaShop

1. 屬性注入 (Field Injection) ❌ 不推薦

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    @Autowired // 直接在屬性上請求注入
    private TeaShop teaShop;

    public void drink() {
        System.out.println("顧客喝到了:" + teaShop.makeBubbleTea());
    }
}
  • ✅ 優點:寫法最簡單,程式碼最少,直接在屬性 (Field) 上加上 @Autowired 註解(Annotation)。
  • ❌ 缺點:
    • 隱藏依賴:無法一眼看出這個類別有哪些必要的依賴。
    • 不易測試:在進行單元測試時,很難在不啟動 Spring 的情況下建立 CustomerService 物件並模擬 (Mock) teaShop
    • 可能 NullPointerException:如果 Spring 因為某些原因沒有成功注入,teaShop 就會是 null

2. Setter 注入 (Setter Injection) ⚠️ 少用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private TeaShop teaShop;

    @Autowired // 透過 setter 方法注入
    public void setTeaShop(TeaShop teaShop) {
        this.teaShop = teaShop;
    }

    public void drink() {
        System.out.println("顧客喝到了:" + teaShop.makeBubbleTea());
    }
}
  • ✅ 優點:提供了一個可以選擇性設定依賴的彈性。
  • ❌ 缺點:
    • 無法保證依賴在使用前一定被注入,物件可能處於不完整的狀態。
    • 無法將依賴宣告為 final,代表它在建立後可能被修改。

3. 建構子注入 (Constructor Injection) ✅ 官方推薦

import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    // 可以宣告為 final,確保它不會被修改
    private final TeaShop teaShop;

    // 透過建構子請求依賴
    // 當類別只有一個建構子時,@Autowired 可以省略
    public CustomerService(TeaShop teaShop) {
        this.teaShop = teaShop;
    }

    public void drink() {
        System.out.println("顧客喝到了:" + teaShop.makeBubbleTea());
    }
}
  • ✅ 優點:
    • 保證依賴完整性:物件在被建立時,其必要依賴必須先被傳入,確保物件一建立就是一個「完整」可用的狀態,避免了 NullPointerException
    • 依賴關係一目了然:打開程式碼,一看建構子就知道這個類別需要哪些東西才能活下去,非常清晰。
    • 極易進行單元測試:在單元測試中,你可以輕易地 new CustomerService(new MockTeaShop()),傳入一個假的 TeaShop 來進行測試,完全不需要 Spring 容器的介入。
    • 支援不可變性 (Immutability):可以將依賴宣告為 final,確保它在物件生命週期內不會被意外修改,這在多執行緒環境下更安全。
  • ✅ 官方建議:最佳實踐。

三種方式比較表

注入方式 優點 缺點 推薦度
屬性注入 寫法最簡單 隱藏依賴、不易測試 ❌ 不推薦
Setter 注入 彈性高 無法保證依賴完整 ⚠️ 少數情境使用
建構子注入 明確依賴、可測試、安全 程式碼稍多 ✅ 官方推薦

使用 @Configuration@Bean 建立自訂 Bean

當你需要管理的物件來自第三方函式庫(你無法修改其原始碼),就無法使用 @Service 等註解 (Annotation)。這時,可以使用 @Configuration + @Bean 來手動註冊。

// 假設這是一個來自第三方套件的類別
public class CoffeeMachine {
    public String brew() {
        return "一杯由高級咖啡機沖泡的咖啡";
    }
}
// 建立一個設定檔類別
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // 告訴 Spring:這是一個設定檔
public class AppConfig {

    @Bean // 告訴 Spring:請將這個方法回傳的物件,當作一個 Bean 來管理
    public CoffeeMachine coffeeMachine() {
        // 這裡可以進行複雜的初始化設定
        return new CoffeeMachine();
    }
}
import org.springframework.stereotype.Service;

// 在服務中使用它
@Service
public class BaristaService {
    private final CoffeeMachine coffeeMachine;

    // 同樣使用建構子注入
    public BaristaService(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    public void serveCoffee() {
        System.out.println("咖啡師為您送上:" + coffeeMachine.brew());
    }
}

總結:為什麼要使用依賴注入?

  1. 降低耦合度 (Decoupling):你的類別不再寫死 (Hard-coding) 對特定實作的依賴,更換實作變得輕而易舉。
  2. 提升可測試性 (Testability):可以輕鬆地傳入模擬 (Mock) 物件,讓單元測試變得乾淨又高效。
  3. 程式碼更清晰 (Clarity):依賴關係集中在建構子中,一目了然。
  4. 集中管理 (Centralized Management):所有物件的生命週期和組態都由 Spring IoC 容器統一管理。

相關資料來源


上一篇
Day09 - Spring Boot Properties 實戰與進階
下一篇
Day11 - Spring Boot Annotation 簡介
系列文
spring boot 3 學習筆記17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言