第四章:@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);
}
}
在傳統方式中,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;
}
// ...
}
構造函數注入的優點:
- 確保物件創建時所有必要的依賴都已注入
- 便於單元測試
- 不可變性:依賴可以標記為 final
- 循環依賴在編譯時就能發現
Setter 注入:
@Service
public class UserService {
private UserRepository userRepository;
// Setter 注入
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
Setter 注入適用於可選的依賴,或者在運行時可能需要改變的依賴。
欄位注入:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// ...
}
欄位注入看起來最簡潔,但有一些缺點:
- 不便於測試
- 無法聲明不可變欄位
- 可能導致循環依賴問題被隱藏
最佳實踐:優先使用構造函數注入。對於可選依賴,可以使用 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(/* 配置 */);
}
}
@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();
}
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> {
// ...
}
使用 @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;
}
}
選擇指南:
- 如果類是你自己擁有的原始碼,使用
@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();
}
}
各作用域的特點:
- 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();
}
}
使用 @primary 處理歧義:
@Configuration
public class PrimaryConfig {
@Bean
@Primary
public FileStorage primaryStorage() {
return new S3FileStorage();
}
@Bean
public FileStorage localStorage() {
return new LocalFileStorage();
}
}
使用 @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 */);
}
}
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;
}
}
這個範例展示了:
- 多個相同類型 Bean 的處理(使用
@Primary) - 基於環境的 Bean 選擇(使用
@Profile) - 構造函數注入
- Bean 之間的依賴關係
Top comments (0)