iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

從 Laravel 到 Spring Boot:30 天打造縮網址服務系列 第 4

Day4 - Spring Boot 專案結構與 IoC/DI 容器 vs Laravel Service Container

  • 分享至 

  • xImage
  •  

版本選擇:Java 25 + Spring Boot 4.1.x

這系列會用 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 那次 javaxjakarta 大遷移以來變動最大的一次改版,跟網路上多數還停留在 3.x 世代的教學會有落差,中間有一段「原本想選穩妥的舊版,結果被現實逼著跳新版」的心路歷程——完整的來龍去脈跟踩雷細節寫成了另一篇輕鬆版外傳,這裡先講當下需要知道的重點就好:

  • Spring Security 7 的預設行為改變(例如 API 端點的 CSRF 保護預設變成開啟),細節留到 Day13
  • 測試相關的變更(@MockBean/@SpyBean 被移除),細節留到 Day19、Day20
  • 其餘像 Jackson 3 這類底層套件升級,遇到的當下再個別說明

Spring Boot 專案骨架

用 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/DI 容器:Spring 註解式注入 vs Laravel Service Container

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() 那種「手動指定」的做法。

Bean 的生命週期:回扣 Day02

還記得 Day02 說的「長駐進程 vs 每請求重建」嗎?這就是為什麼 Spring 的 Bean 預設是 Singleton——整個應用只會建立一次 LinkService 的實例,之後所有請求共用同一個物件,跟 Laravel 每個 request 常常重新解析容器不一樣。這也是為什麼在 Bean 裡面不能隨便存放跟單一請求相關的狀態(例如把「這次請求的使用者」存成 Bean 的成員變數)——因為這個 Bean 會被下一個請求、下下一個請求繼續共用,狀態會互相污染。這點會在後面講 Middleware/Interceptor(Day12)時再提醒一次。


上一篇
Day3 - 建置工具 Maven/Gradle vs Composer
下一篇
Day5 - Spring MVC Controller/Routing vs Laravel Route/Controller
系列文
從 Laravel 到 Spring Boot:30 天打造縮網址服務9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言