DEV Community

Cyrus Tse
Cyrus Tse

Posted on

Spring Boot 4 入門教學 - Part 4 (@Bean 與 IoC 容器)

第四章:@bean 與 IoC 容器

4.1 前言:理解控制反轉與依賴注入

在這一章節中,我們將深入學習 Spring Framework 最重要的兩個概念:控制反轉(Inversion of Control,簡稱 IoC)和依賴注入(Dependency Injection,簡稱 DI)。這些概念是 Spring 的核心,理解它們對於成為一個優秀的 Spring 開發者至關重要。

很多初學者對這些概念感到困惑,因為它們聽起來很抽象。但實際上,這些概念非常簡單,並且在我們的日常開發中處處可見。讓我們通過具體的範例來理解這些概念。

4.2 控制反轉(IoC)的概念

控制反轉是一種軟體設計原則,它顛倒了傳統的程式控制流程。在傳統的程式設計中,我們自己創建和管理物件的依賴關係;而在 IoC 中,這個控制權被反轉給了框架(這裡就是 Spring 容器)。

讓我們通過一個簡單的對比來理解這個概念:

// 傳統方式:類自己負責創建和管理依賴
public class UserService {
    private UserRepository userRepository;

    public UserService() {
        // UserService 負責創建 UserRepository
        this.userRepository = new UserRepository();
    }

    public void createUser(String name) {
        userRepository.save(name);
    }
}

// IoC 方式:依賴由外部提供
@Service
public class UserService {

    private final UserRepository userRepository;

    // 依賴通過構造函數注入
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String name) {
        userRepository.save(name);
    }
}
Enter fullscreen mode Exit fullscreen mode

在傳統方式中,UserService 直接創建 UserRepository,這導致了以下問題:

  • 緊密耦合:UserService 依賴於 UserRepository 的具體實現
  • 難以測試:無法輕鬆地替換 UserRepository 為 Mock 物件
  • 難以維護:任何 UserRepository 的變化都可能影響 UserService

而在 IoC 方式中,UserRepository 的實例由外部(Spring 容器)提供,這解決了上述所有問題。

4.3 依賴注入(DI)的實現方式

Spring Framework 提供了多種依賴注入的方式:構造函數注入、Setter 注入和欄位注入。讓我們逐一學習。

構造函數注入(推薦)

@Service
public class UserService {

    private final UserRepository userRepository;

    // 構造函數注入
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

構造函數注入的優點:

  • 確保物件創建時所有必要的依賴都已注入
  • 便於單元測試
  • 不可變性:依賴可以標記為 final
  • 循環依賴在編譯時就能發現

Setter 注入

@Service
public class UserService {

    private UserRepository userRepository;

    // Setter 注入
    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Setter 注入適用於可選的依賴,或者在運行時可能需要改變的依賴。

欄位注入

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

欄位注入看起來最簡潔,但有一些缺點:

  • 不便於測試
  • 無法聲明不可變欄位
  • 可能導致循環依賴問題被隱藏

最佳實踐:優先使用構造函數注入。對於可選依賴,可以使用 Setter 注入。

4.4 @bean 註解的深度解析

@Bean 註解用於聲明 Spring 容器應該管理的 Bean。與 @Component 等元件掃描方式不同,@Bean 允許你更精細地控制 Bean 的創建過程。

讓我們創建一個複雜的 @Bean 範例:

package com.example.myfirstapp.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    /**
     * @Bean 註解的方法會被 Spring 容器調用
     * 返回值會被註冊為容器中的 Bean
     */
    @Bean
    @Primary // 標記為主要 Bean,優先注入
    @Scope("singleton") // 預設為單例
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(5);
        dataSource.setConnectionTimeout(30000);
        dataSource.setIdleTimeout(600000);
        dataSource.setMaxLifetime(1800000);
        return dataSource;
    }

    /**
     * 自定義 Bean 名稱
     */
    @Bean(name = "customDataSource")
    public DataSource customDataSource() {
        // 自定義配置
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    /**
     * 工廠 Bean
     */
    @Bean
    public ConnectionFactory connectionFactory() {
        return new ConnectionFactory(/* 配置 */);
    }
}
Enter fullscreen mode Exit fullscreen mode

@Bean 註解支援多種屬性:

  • name:指定 Bean 的名稱(預設為方法名稱)
  • autowireMode:配置自動注入模式
  • initMethod:指定初始化方法
  • destroyMethod:指定銷毀方法
  • autowireCandidate:標記是否可以被自動注入
@Bean(
    name = {"myBean", "myAlternativeBean"},
    autowireMode = Autowire.BY_TYPE,
    initMethod = "initialize",
    destroyMethod = "cleanup",
    autowireCandidate = true
)
public MyService myService() {
    return new MyService();
}
Enter fullscreen mode Exit fullscreen mode

4.5 @Component vs @bean:如何選擇?

在 Spring 中,有兩種主要的方式來註冊 Bean:@Component(配合元件掃描)和 @Bean(在 @Configuration 類中)。讓我們學習如何選擇合適的方式。

使用 @Component 的時機:

// 簡單的元件類
@Component
public class DateFormatter {
    public String formatDate(java.time.LocalDate date) {
        return date.format(java.time.format.DateTimeFormatter.ISO_DATE);
    }
}

// Service 類
@Service
public class UserService {
    // ...
}

// Repository 類
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

使用 @Bean 的時機:

@Configuration
public class ThirdPartyConfig {

    // 整合第三方庫
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    // 自定義 Bean 創建邏輯
    @Bean
    public CacheManager cacheManager() {
        // 複雜的創建邏輯
        return new CustomCacheManager(/* 各種配置 */);
    }

    // 多個相同類型的 Bean
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("messages");
        return source;
    }
}
Enter fullscreen mode Exit fullscreen mode

選擇指南

  • 如果類是你自己擁有的原始碼,使用 @Component 及其特化版本(@Service@Repository
  • 如果類來自第三方庫或需要複雜的創建邏輯,使用 @Bean
  • 如果需要對 Bean 的創建過程有細粒度控制,使用 @Bean

4.6 Bean 的作用域

Spring 容器中的 Bean 可以有不同的作用域。理解這些作用域對於正確設計應用程式非常重要。

@Configuration
public class BeanScopeConfig {

    // 預設作用域:單例
    @Bean
    public SingletonService singletonService() {
        return new SingletonService();
    }

    // 原型作用域:每次注入都會創建新實例
    @Bean
    @Scope("prototype")
    public PrototypeService prototypeService() {
        return new PrototypeService();
    }

    // 請求作用域(Web 應用)
    @Bean
    @Scope("request")
    public RequestScopedService requestScopedService() {
        return new RequestScopedService();
    }

    // 會話作用域(Web 應用)
    @Bean
    @Scope("session")
    public SessionScopedService sessionScopedService() {
        return new SessionScopedService();
    }
}
Enter fullscreen mode Exit fullscreen mode

各作用域的特點:

  • singleton(單例):整個容器中只有一個實例。這是 Spring Boot 的預設作用域,適用於無狀態的服務類。
  • prototype(原型):每次注入或從容器獲取時都會創建新實例。適用於有狀態的物件。
  • request:每個 HTTP 請求創建一個新實例。僅在 Web 環境中有效。
  • session:每個 HTTP 會話創建一個新實例。僅在 Web 環境中有效。

4.7 依賴注入的進階主題

讓我們學習一些進階的依賴注入主題。

使用 @Qualifier 處理多個相同類型的 Bean

@Configuration
public class MultiBeanConfig {

    @Bean
    public MessageService emailService() {
        return new EmailMessageService();
    }

    @Bean
    public MessageService smsService() {
        return new SmsMessageService();
    }

    @Bean
    public MessageService pushService() {
        return new PushMessageService();
    }
}

@Service
public class NotificationService {

    private final MessageService messageService;

    // 使用 @Qualifier 指定具體的 Bean
    @Autowired
    public NotificationService(@Qualifier("emailService") MessageService messageService) {
        this.messageService = messageService;
    }

    // 使用 @Primary 標記首選 Bean
    @Bean
    @Primary
    public MessageService defaultMessageService() {
        return new EmailMessageService();
    }
}
Enter fullscreen mode Exit fullscreen mode

使用 @primary 處理歧義

@Configuration
public class PrimaryConfig {

    @Bean
    @Primary
    public FileStorage primaryStorage() {
        return new S3FileStorage();
    }

    @Bean
    public FileStorage localStorage() {
        return new LocalFileStorage();
    }
}
Enter fullscreen mode Exit fullscreen mode

使用 @profile 根據環境選擇 Bean

@Configuration
public class ProfileConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        // 開發環境的資料來源
        return new HikariDataSource(/* dev config */);
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        // 生產環境的資料來源
        return new HikariDataSource(/* prod config */);
    }
}
Enter fullscreen mode Exit fullscreen mode

4.8 實戰:構建配置類

讓我們通過一個完整的實戰範例來整合這一章學習的知識:

package com.example.myfirstapp.config;

import com.example.myfirstapp.service.CalculatorService;
import com.example.myfirstapp.service.impl.SimpleCalculatorService;
import com.example.myfirstapp.service.impl.ScientificCalculatorService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;

@Configuration
public class CalculatorConfig {

    /**
     * 簡單計算機服務 - 預設 Bean
     */
    @Bean
    @Primary
    @Profile({"default", "dev", "test"})
    public CalculatorService simpleCalculatorService() {
        return new SimpleCalculatorService();
    }

    /**
     * 科學計算機服務 - 生產環境
     */
    @Bean
    @Profile("prod")
    public CalculatorService scientificCalculatorService() {
        return new ScientificCalculatorService();
    }

    /**
     * 自定義日誌配置
     */
    @Bean
    public LoggingService loggingService(CalculatorService calculatorService) {
        return new LoggingService(calculatorService);
    }
}

// CalculatorService 接口
interface CalculatorService {
    int add(int a, int b);
    int subtract(int a, int b);
    int multiply(int a, int b);
    int divide(int a, int b);
}

// LoggingService 類
class LoggingService {

    private final CalculatorService calculatorService;

    public LoggingService(CalculatorService calculatorService) {
        this.calculatorService = calculatorService;
    }

    public int logAndCalculate(String operation, int a, int b) {
        System.out.println("執行操作: " + operation + ",參數: " + a + ", " + b);
        int result = switch (operation) {
            case "add" -> calculatorService.add(a, b);
            case "subtract" -> calculatorService.subtract(a, b);
            case "multiply" -> calculatorService.multiply(a, b);
            case "divide" -> calculatorService.divide(a, b);
            default -> throw new IllegalArgumentException("不支持的操作: " + operation);
        };
        System.out.println("結果: " + result);
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

這個範例展示了:

  • 多個相同類型 Bean 的處理(使用 @Primary
  • 基於環境的 Bean 選擇(使用 @Profile
  • 構造函數注入
  • Bean 之間的依賴關係

Top comments (0)