skip to content
BlogZzz

[Learn] constructor injection 與 singleton Bean 狀態

/ 7 min read

Updated:
Table of Contents

上一篇介紹了 DI 與 Spring Bean,這篇接著用實作補充兩個容易混淆的地方:Spring 如何透過 constructor injection 組裝多個依賴,以及 singleton Bean 是否需要 static 欄位才能共用狀態

用 constructor injection 組裝多個依賴

程式碼來說,建構子要求兩個參數:

@Repository
public class ProductRepository {
}
@Repository
public class UserRepository {
}
@Service
public class ProductService {
private final ProductRepository productRepository;
private final UserRepository userRepository;
@Autowired
public ProductService(
ProductRepository productRepository,
UserRepository userRepository
) {
this.productRepository = productRepository;
this.userRepository = userRepository;
}
}

Spring Boot 啟動並掃描到這些類別後,做的事情可以想成下面這樣

這裡假設 ProductRepository、UserRepository 和 ProductService 都在 Spring 的 component scan 範圍內,而且各自只找到一個符合型別的 Bean

@Repository 和 @Service 會提供元件註冊資訊,但類別仍要位於掃描範圍內,Spring 才會建立並管理它們;如果漏了掃描設定,或某個依賴沒有註冊成 Bean,Spring 就無法完成這次注入

ProductRepository repo = new ProductRepository();
UserRepository userRepo = new UserRepository();
ProductService service = new ProductService(repo, userRepo);
// 接著由 Spring 容器管理這些物件

直接寫 new ProductService() 會編譯失敗,因為這個類別沒有無參數建構子

如果交給 Spring 建立,Spring 會:

  1. 發現要建立 ProductService 這個 Bean
  2. 看到建構子需要 ProductRepository 和 UserRepository
  3. 找到對應的 Bean;需要時先建立它們
  4. 把這兩個 Bean 傳入建構子,建立 ProductService

所以可以想成:宣告「我需要什麼」,Spring 負責準備並傳進來

這個範例只有一個 constructor,因此 @Autowired 可以省略,Spring 仍會使用這個 constructor 注入依賴

如果類別有多個 constructor,才需要明確標示要讓 Spring 使用哪一個,或透過其他設定說明如何建立這個 Bean

在 Controller 要用 ProductService 時怎麼寫? 同樣用建構子接收:

@RestController
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
// 方法內就可以使用 this.productService
}

Spring 也會幫你呼叫這個 Controller 的建構子,把準備好的 ProductService 傳進來

因此整條依賴關係:

Terminal window
ProductController → ProductService → 兩個 Repository

都由 Spring 建立、組裝,你不用自己寫 new ProductService(…)

singleton Bean 不需要用 static 共用狀態

閱讀了某些 DI 的文章, 對這一段程式碼的 static 有點疑問

@Repository
public class ListProductRepository implements IProductRepository {
private static final List<ProductPO> productList = new ArrayList<>();
// ...
public ProductPO getOneById(String id) {
return productList
.stream()
.filter(p -> p.getId().equals(id))
.findFirst()
.orElse(null);
}
public void deleteById(String id) {
productList.removeIf(p -> p.getId().equals(id));
}
// ...
}

static 欄位屬於類別,所有 Repository 實例共用同一份 List

假設建立兩個 Repository:

var a = new ListProductRepository();
var b = new ListProductRepository();
  • private static final:a、b 共用同一份 List;透過 a 加入的資料,b 也看得到
  • private final:搭配這裡的 = new ArrayList<>(),a、b 各自擁有一份 List,資料互不影響

如果交給 Spring 管理,而且使用預設的 singleton scope,同一容器內這個 Bean 本來就只有一個實例,因此用:

private final List<ProductPO> productList = new ArrayList<>();

就能讓使用這個 Repository Bean 的程式共用資料,不需要再靠 static

@Repository
public class ListProductRepository {
private final List<ProductPO> productList = new ArrayList<>();
}

Spring 把 同一個 R 注入給兩個 Service:

ProductService ──→ Repository R ──→ 一份 productList
↑
AdminService ────────────┘

所以:

  • ProductService 透過 R 新增產品
  • AdminService 透過 R 查詢,就能看到那個產品

因為大家用的是同一個物件,所以即使沒有 static,也共用同一份資料

這裡的 singleton 指同一個 Spring ApplicationContext 內,同一個 Bean definition 預設只建立一個實例

因此 ProductService 和 AdminService 注入同一個 Repository Bean 時,會操作那個實例裡的同一份 productList

這不代表不同的 Spring container、不同的應用程式程序,或不同伺服器上的 Repository 也會共用這份 List;每個 container 都有自己的 Bean 實例與記憶體

另外,singleton 只描述物件的生命週期與實例數量,不會讓物件內的資料自動變安全或持久

這個 ArrayList 是可變狀態,多個 request 可能同時讀寫它,而 ArrayList 本身不保證這類並行操作安全;應用程式重新啟動後,記憶體中的資料也會消失

因此這種寫法適合用來示範同一個 Bean 的狀態共享,正式儲存產品資料通常會交給資料庫等持久化儲存,並依實際需求處理並行存取

補充

component scsn

component scan(元件掃描)是 Spring 啟動時搜尋哪些類別要交給它管理的機制

例如類別標上 @Service 或 @Repository,Spring 找到它們後,就能建立 Bean,再將 Bean 注入其他物件:

@Repository
public class ProductRepository {
}
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
}

Spring Boot 通常會從主程式類別所在的 package 開始掃描子 package

假設主程式在 com.example,像 com.example.repository、com.example.service 通常都會

被掃描;放在其他 package 的類別可能找不到,除非另外設定掃描範圍

所以,標註 @Service 本身不代表 Spring 一定會管理那個類別;它還必須位於掃描範圍內