這系列會用 Java 25(LTS) 搭配 Spring Boot 4.1.x。簡短版原因:Spring Boot 3.5 已經在 2026 年 6 月底走到開源版本的 EOL(沒有免費安全更新了),現在只剩 4.0.x / 4.1.x 在官方支援範圍內,既然這個專案的目標是真的要上線、之後還要開源讓人自建,從一個已經停止維護的版本起步不合理。
要注意的是:Spring Boot 4 是自 Spring 6 那次 javax → jakarta 大遷移以來變動最大的一次改版,跟網路上多數還停留在 3.x 世代的教學會有落差,中間有一段「原本想選穩妥的舊版,結果被現實逼著跳新版」的心路歷程——完整的來龍去脈跟踩雷細節寫成了另一篇輕鬆版外傳,這裡先講當下需要知道的重點就好:
@MockBean/@SpyBean 被移除),細節留到 Day19、Day20 講用 Spring Initializr 生出來的最小專案,大致長這樣:
url-shortener/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/com/example/shortener/
│ │ │ ├── UrlShortenerApplication.java ← 帶 @SpringBootApplication 的進入點
│ │ │ ├── core/ ← 核心邏輯:Entity、Service、Repository
│ │ │ └── web/ ← Controller、Web 相關
│ │ └── resources/
│ │ └── application.yml ← 設定檔
│ └── test/
│ └── java/com/example/shortener/
core/web 這個分包不是 Spring Boot 的規範,是這系列自己的架構決定:30 天開發期間先用單一 Maven module + package 分包,不引入 multi-module 建置複雜度(現在才 Day04,還沒學到 Maven 基礎太多);等完賽後真的要開源拆分時,再把 core package 升級成獨立 module。這樣自建的人 30 天後看到的專案,職責邊界從第一天就是清楚的,不用等到之後重構。
跟 Laravel 專案結構對照:
| Laravel | Spring Boot |
|---|---|
artisan |
沒有對應的命令列工具,用 IDE 或 mvn 指令 |
routes/api.php |
沒有集中的路由檔,路由資訊分散標註在各 Controller 上(Day05 提過) |
app/Http/Controllers |
Controller 類別,通常放在 web/controller package |
app/Models |
JPA Entity 類別 |
.env / config/*.php |
application.yml |
app/Providers |
@Configuration 類別 |
最大的第一印象差異:Laravel 是「檔案與資料夾的慣例決定角色」,Spring Boot 更多是「用註解(annotation)標註角色」,類別放在哪個資料夾比較不重要,重要的是類別上貼了什麼註解。
IoC(控制反轉) 的概念在兩邊是一樣的:物件不自己 new 出依賴,而是由外部的容器負責建立、組裝、注入進來。差異在「怎麼告訴容器某個類別該怎麼被管理」:
Laravel:容器靠建構子的型別提示自動解析依賴,介面則需要在 Service Provider 手動 bind():
// AppServiceProvider
public function register(): void
{
$this->app->bind(LinkRepositoryInterface::class, EloquentLinkRepository::class);
}
class LinkService
{
public function __construct(
private LinkRepositoryInterface $repository
) {}
}
Spring Boot:一樣靠建構子注入自動解析,但類別要先用註解「自我宣告」身份,容器啟動時掃描並註冊成 Bean:
@Repository
class JpaLinkRepository implements LinkRepository { ... }
@Service
class LinkService {
private final LinkRepository repository;
LinkService(LinkRepository repository) {
this.repository = repository;
}
}
兩邊的建構子注入邏輯幾乎一模一樣,真正的差異在「介面要對應到哪個實作」這件事:Laravel 集中寫在 Service Provider,Spring 則是分散在各類別自己身上的註解(@Component/@Service/@Repository 都是「這是一個 Bean」的標記,差異只在語意上代表哪一層)。如果同一個介面有多個實作,才需要額外用 @Primary 或 @Qualifier 明確指定——這時候才比較接近 Laravel bind() 那種「手動指定」的做法。
還記得 Day02 說的「長駐進程 vs 每請求重建」嗎?這就是為什麼 Spring 的 Bean 預設是 Singleton——整個應用只會建立一次 LinkService 的實例,之後所有請求共用同一個物件,跟 Laravel 每個 request 常常重新解析容器不一樣。這也是為什麼在 Bean 裡面不能隨便存放跟單一請求相關的狀態(例如把「這次請求的使用者」存成 Bean 的成員變數)——因為這個 Bean 會被下一個請求、下下一個請求繼續共用,狀態會互相污染。這點會在後面講 Middleware/Interceptor(Day12)時再提醒一次。