skip to content
BlogZzz

[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 的關係

名稱這裡的意思
IoCInversion of Control,把物件建立與組裝等控制權交給 container
DIcontainer 或呼叫端把需要的依賴傳入物件
Bean由 Spring IoC container 管理的物件
ApplicationContextSpring 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資料存取元件
@Controllerweb 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 後呼叫

Terminal window
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 流程和物件組裝連在一起