[Learn] constructor injection 與 singleton Bean 狀態
/ 7 min read
Updated:Table of Contents
上一篇介紹了 DI 與 Spring Bean,這篇接著用實作補充兩個容易混淆的地方:Spring 如何透過 constructor injection 組裝多個依賴,以及 singleton Bean 是否需要 static 欄位才能共用狀態
用 constructor injection 組裝多個依賴
程式碼來說,建構子要求兩個參數:
@Repositorypublic class ProductRepository {}
@Repositorypublic class UserRepository {}
@Servicepublic 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 會:
- 發現要建立 ProductService 這個 Bean
- 看到建構子需要 ProductRepository 和 UserRepository
- 找到對應的 Bean;需要時先建立它們
- 把這兩個 Bean 傳入建構子,建立 ProductService
所以可以想成:宣告「我需要什麼」,Spring 負責準備並傳進來
這個範例只有一個 constructor,因此 @Autowired 可以省略,Spring 仍會使用這個 constructor 注入依賴
如果類別有多個 constructor,才需要明確標示要讓 Spring 使用哪一個,或透過其他設定說明如何建立這個 Bean
在 Controller 要用 ProductService 時怎麼寫? 同樣用建構子接收:
@RestControllerpublic class ProductController { private final ProductService productService;
@Autowired public ProductController(ProductService productService) { this.productService = productService; }
// 方法內就可以使用 this.productService}Spring 也會幫你呼叫這個 Controller 的建構子,把準備好的 ProductService 傳進來
因此整條依賴關係:
ProductController → ProductService → 兩個 Repository都由 Spring 建立、組裝,你不用自己寫 new ProductService(…)
singleton Bean 不需要用 static 共用狀態
閱讀了某些 DI 的文章, 對這一段程式碼的 static 有點疑問
@Repositorypublic 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
@Repositorypublic 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 一定會管理那個類別;它還必須位於掃描範圍內