[Learn] Spring Boot DI 與 Bean:物件是誰建立並注入的
/ 10 min read
Table of Contents
上一篇的 Controller 需要 Service,Service 又需要 Repository
程式沒有自己建立這整串物件,卻可以正常接收 request
這篇先用一般 Java 解釋 DI,再回頭看 Spring Bean 如何註冊與注入,最後在同一個專案加入一個 Clock 範例
DI 先從 constructor 理解
假設 Service 自己選定並建立 Repository
public class StudentService { private final StudentRepository studentRepository = new InMemoryStudentRepository();}這是概念片段,不是要替換上一篇的完整 Service
Service 同時決定自己需要什麼,以及具體實作如何建立
改成從 constructor 接收依賴
public class StudentService { private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) { this.studentRepository = studentRepository; }}建立物件的人就能決定傳入哪一種 Repository
StudentRepository repository = new InMemoryStudentRepository();StudentService service = new StudentService(repository);BootController controller = new BootController(service);這段只是手動組裝的示意,不會因此啟動 web server 或註冊 HTTP route
即使沒有 Spring,這也已經是 DI,也就是 Dependency Injection
DI 的重點是把依賴從外部交給物件,constructor injection 是其中一種方式
IoC、DI 與 Bean 的關係
| 名稱 | 這裡的意思 |
|---|---|
| IoC | Inversion of Control,把物件建立與組裝等控制權交給 container |
| DI | container 或呼叫端把需要的依賴傳入物件 |
| Bean | 由 Spring IoC container 管理的物件 |
| ApplicationContext | Spring container 的主要介面之一,負責 Bean 與相關應用程式服務 |
在上一篇的預設設定下,可以用以下方式理解啟動過程
SpringApplication.run(...) ↓建立 ApplicationContext,讀取設定並註冊 Bean definition ↓依照依賴關係建立與初始化 Bean ↓InMemoryStudentRepository → StudentService → BootController ↓建立 web request mapping,應用程式開始提供服務這是理解依賴的簡化流程,不代表所有 Bean 都固定依照檔案順序建立,也不包含 lazy initialization 或其他 scope 的特殊時機
Spring DI 的定義與 constructor injection 可對照 官方 DI 文件
Bean 是被管理的物件,不是 class 名稱,也不是 annotation 本身
StudentService 是 class,Spring 依設定建立並管理的 instance 才是這裡說的 Bean
一般 new Student(...) 產生的資料物件不會自動變成 Bean,response DTO 也通常不需要交給 container 管理
Component scanning 如何找到 Bean
上一篇使用以下 annotation
| annotation | 常用角色 |
|---|---|
@Component | 一般 Spring component |
@Service | 業務流程元件 |
@Repository | 資料存取元件 |
@Controller | web controller |
@RestController | 預設把回傳值寫入 response body 的 controller |
@Service、@Repository 與 @Controller 都是 @Component 的特化,@RestController 又包含 @Controller
元件位於掃描範圍內、符合註冊條件時,Spring 才會把它們納入管理
所以 DI 成立需要兩端都準備好:接收依賴的 Service 由 Spring 管理,提供依賴的 Repository 也必須已註冊成可用的 Bean
只在一般 object 的 field 寫 @Autowired,不會自動把那個 object 交給 Spring
Spring 怎麼決定要注入哪一個物件
回到上一篇的 constructor
public StudentService(StudentRepository studentRepository) { this.studentRepository = studentRepository;}Spring 會根據這個 injection point 的型別與 qualifier 等資訊解析依賴
本例只有一個符合 StudentRepository 型別的 Bean,就是 InMemoryStudentRepository,因此可以注入
只有一個 constructor 時,即使沒有 @Autowired,Spring 也會使用它,可參考 Autowired 官方說明
constructor injection 的實際好處是能看出必要依賴、用 final 保存 reference,也容易在一般 Java 程式或測試中自行組裝
不需要為了 DI 強制替每一個 Service 建立 interface,這篇 Controller 直接依賴 StudentService 也可以
Repository 使用 interface,是為了表達資料存取契約並允許替換實作
用 Bean 註冊無法修改的 class
自己寫的 class 可以加 @Component 或 @Service
但像 JDK 的 Clock,不適合也不能直接去修改它的原始碼加 Spring annotation
這時可以透過 configuration 中的 @Bean method 定義建立方式
在上一篇專案的 com.example.bootdemo package 新增 TimeConfiguration.java
package com.example.bootdemo;
import java.time.Clock;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)public class TimeConfiguration { @Bean public Clock appClock() { return Clock.systemUTC(); }}@Configuration 讓這個 class 提供 Bean definition,@Bean 告訴 Spring 用這個 method 的回傳物件作為 Bean
Bean 的預設名稱是 method 名稱,所以這個 Bean 叫 appClock,型別是 Clock
Clock.systemUTC() 提供以 UTC zone 表示的系統時鐘,可以透過 Clock 介面讀取當下時間
這裡使用 proxyBeanMethods = false,不依賴 configuration 代理去攔截 @Bean method 之間的呼叫
之後若某個 @Bean method 需要其他 Bean,優先透過 method parameter 注入,不要把直接呼叫 appClock() 當成向 container 取得既有 Bean
@Bean 的 method parameter 注入方式可參考 官方 Java configuration 文件
把 Clock 注入新的 API
新增 TimeResponse.java
package com.example.bootdemo;
public record TimeResponse(String now) {}新增 TimeController.java
package com.example.bootdemo;
import java.time.Clock;import java.time.Instant;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/api/time")public class TimeController { private final Clock clock;
public TimeController(Clock clock) { this.clock = clock; }
@GetMapping public TimeResponse now() { return new TimeResponse(Instant.now(clock).toString()); }}重新啟動 application 後呼叫
curl 'http://localhost:8080/api/time'預期得到包含 ISO-8601 時間字串的 JSON,例如
{ "now": "2026-09-10T02:30:00Z" }實際值取決於發送 request 的時間,Z 表示 UTC
這個 API 只是示範 Bean 注入,沒有額外業務流程,因此直接在 Controller 使用 Clock 即可
需要固定時間來驗證行為時,也可以完全不啟動 Spring,自己組裝 Controller
Clock fixedClock = Clock.fixed( Instant.parse("2026-09-10T00:00:00Z"), java.time.ZoneOffset.UTC);TimeController controller = new TimeController(fixedClock);
System.out.println(controller.now().now());// 2026-09-10T00:00:00Z這是放在一般 Java method 內的示意片段,需要 import Clock 與 Instant
把時間來源抽成 constructor dependency,呼叫端就可以決定使用系統時鐘或固定時鐘
同一型別有多個 Bean 時
假設未來同時註冊 appClock 與 reportClock,兩者型別都是 Clock
當單一 injection point 無法唯一選出候選者時,可能出現 NoUniqueBeanDefinitionException
需要指定用途時,可以修改 TimeController 的 constructor,明確選擇既有的 appClock
public TimeController( @Qualifier("appClock") Clock clock) { this.clock = clock;}使用這段時要在檔案加上 import
import org.springframework.beans.factory.annotation.Qualifier;也可以在其中一個 Bean definition 加上 @Primary,讓它在單值注入時作為優先候選
@Primary 適合指定預設實作,@Qualifier 適合表達某個 injection point 要哪一種用途,不需要為了處理注入問題把依賴改成 static
相關選擇規則可參考 Primary 文件 與 Qualifier 文件
預設 singleton 不代表 thread-safe
Spring Bean 預設是 singleton scope,意思是同一個 container 中,同一份 Bean definition 對應一個共享 instance
不是整個 JVM 只能有一個該 class 的物件,也不是每次 HTTP request 都建立新的 Controller 或 Service
所以不要把某次 request 的資料寫在 singleton Service 的 mutable field 裡
// 反例:不同 request 會共用這個欄位private String currentStudentName;request 專屬資料應透過 method parameter、local variable 或適合的資料結構傳遞
上一篇 Repository 使用不可修改的 Map 與只包含基本資料的 record,只有讀取操作;如果改成可寫入的共享狀態,就必須另外考慮並行存取
final 只限制 field reference 不能重新指派,不保證它指向的物件不可修改,也不會自動保證 thread safety
scope 的正式定義可參考 Spring Bean scopes 文件
注入失敗時先檢查什麼
- 找不到 Bean:檢查實作是否有註冊、是否位於掃描範圍、是否受 profile 或 condition 限制
- 找到多個候選者:檢查是否重複用 component scan 與
@Bean註冊,或需要@Qualifier/@Primary - 自己
new出來的物件沒有注入:改由 container 組裝,或自行把依賴傳進 constructor - A 的 constructor 需要 B,B 又需要 A:重新拆分責任,constructor 循環依賴無法完成正常組裝
回到系列的查詢流程,Controller、Service 與 Repository 描述的是責任分工,DI 描述的是依賴如何交給物件,Bean 則是 Spring 管理這些物件時使用的名稱
讀程式時分別看「這個 method 做什麼」與「這個 constructor 需要誰」,就比較容易把 request 流程和物件組裝連在一起