Spring Boot 入門:給 Next.js/Go 開發者的第一個 REST API
用 Spring Boot 寫一個最小可行的 REST API,對照 Next.js API route 跟 Go Gin handler 的心智模型,搞懂依賴注入(DI)到底在解決什麼問題,以及跟這個部落格本身 actions/services 分層同一個精神的 Controller/Service/Repository 架構。
系列第四篇,也是最後一篇。前三篇打了 Java 語言本身的基礎(語法/JVM、物件導向、Collections/泛型/例外處理),這篇是實際的payoff——用 Spring Boot 寫一個最小可行的 REST API,對照 Next.js API route 跟 Go Gin handler 的心智模型。
一、Spring Boot 是什麼
Spring 是 Java 生態最主流的後端框架,但「原生 Spring」設定非常繁瑣,早年要寫一大堆 XML 設定檔才能跑起來。Spring Boot 是 Spring 官方推出的「約定優於設定」包裝——內建合理預設值、自動組態、內嵌 Tomcat(不用自己另外架 application server),讓你可以像 next dev、go run 那樣,寫完程式碼直接跑起來一個能接 HTTP 請求的服務。
用 Spring Initializr 產生專案骨架(選 Maven、Java、加 Spring Web 依賴),或用 pom.xml 手動加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
二、最小可行的 API:對照你已經會的東西
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
@GetMapping
public List<Article> list() {
return List.of(
new Article("hello-world", "Hello World"),
new Article("second-post", "第二篇文章")
);
}
@GetMapping("/{slug}")
public Article getBySlug(@PathVariable String slug) {
return new Article(slug, "文章標題");
}
}
跟你熟悉的兩種寫法對照:
// Next.js:app/api/articles/route.ts
export async function GET() {
return Response.json([
{ slug: "hello-world", title: "Hello World" },
]);
}
// Go + Gin
router.GET("/api/articles", func(c *gin.Context) {
c.JSON(200, articles)
})
三者做的是同一件事,只是「路由怎麼宣告」的風格不同:Next.js 靠檔案路徑代表路由,Gin 靠明確呼叫 router.GET(...) 註冊,Spring Boot 靠 annotation(@GetMapping)標註在方法上,由框架在啟動時掃描整個 class 自動組裝出路由表。@PathVariable String slug 對應 Next.js 的動態路由參數 [slug],或 Gin 的 c.Param("slug")。
三、依賴注入(DI):Spring 生態的核心概念
這是从 Node/Go 轉過來最需要花時間理解的部分。先看沒有 DI 的寫法:
public class ArticleController {
private ArticleService service = new ArticleService(); // 自己 new
}
問題:ArticleController 自己決定「要用哪一個 ArticleService」,兩者綁死在一起。想在測試時換成一個假的(mock)ArticleService,或想之後根據環境切換不同實作,都要改 ArticleController 的程式碼。
DI 反過來:ArticleController 只宣告「我需要一個 ArticleService」,由誰、怎麼建立這個物件,交給框架決定:
@RestController
public class ArticleController {
private final ArticleService articleService;
// 建構子注入(目前官方推薦的寫法):Spring 啟動時看到這個建構子需要
// 一個 ArticleService,就自動去找一個(或自己 new 一個)塞進來
public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}
@GetMapping("/api/articles")
public List<Article> list() {
return articleService.findAll();
}
}
@Service // 標記這個 class 是一個「服務」,Spring 會自動把它註冊成一個可以被注入的物件
public class ArticleService {
public List<Article> findAll() {
return List.of(new Article("hello-world", "Hello World"));
}
}
@Service 標註的 class,Spring 啟動時會自動 new 一個實例、放進一個叫「容器」(container)的地方管理。ArticleController 的建構子要一個 ArticleService,Spring 就從容器裡把剛剛那個實例拿出來塞進去——這整套「自動幫你組裝物件之間依賴關係」的機制,就是 Spring 生態幾乎所有東西(@RestController、@Service、@Repository、@Component)背後共用的核心引擎。
好處具體在哪:寫測試的時候可以直接把一個假的 ArticleService 塞進 ArticleController,完全不用改 ArticleController 的程式碼,因為它本來就只認得 ArticleService 這個型別(通常實務上會宣告成 interface),不在乎背後實際是哪個實作——這正是第二篇講的「針對介面寫程式」原則,在框架層級被落實成一個自動化機制。
四、分層架構:跟你在 tommy-blog 已經在用的 actions/services 是同一個精神
Spring 生態的慣例分層,剛好可以直接對照這個部落格專案的 actions/、services/ 分工:
| Spring 分層 | 負責什麼 | 對照 tommy-blog |
|---|---|---|
@RestController | 接 HTTP 請求、解析參數、呼叫 Service、組裝回應 | app/api/*/route.ts 或 Server Action 入口 |
@Service | 商業邏輯本身 | services/*.ts 裡的邏輯部分 |
@Repository | 直接跟資料庫溝通(通常搭配 Spring Data JPA,幫你把 SQL 包成方法呼叫) | services/*.ts 裡呼叫 Prisma 的部分 |
@Repository
public interface ArticleRepository extends JpaRepository<ArticleEntity, Long> {
// 繼承 JpaRepository 之後,基本 CRUD 方法(save/findById/findAll...)
// 完全不用自己寫,Spring Data JPA 在執行期自動生成實作
List<ArticleEntity> findByCategory(String category);
}
JpaRepository 這行特別值得注意:你只宣告了一個 interface,連方法本體都沒寫,findByCategory 這種方法 Spring Data JPA 會依照方法名稱自動生成對應的 SQL 查詢——這是 Java 生態很典型的「用大量約定跟自動化換取開發速度」的風格,跟手寫 Prisma query 或 SQL 的體感差蠻多,習慣之後會覺得省事,不習慣之前會覺得「這也太魔法了」。
五、實際跑起來
mvn spring-boot:run
預設會在 localhost:8080 開一個 HTTP server,curl localhost:8080/api/articles 就能拿到剛剛 list() 回傳的資料——體感上跟 next dev、go run ./cmd/server 是同一件事,只是背後 Spring Boot 多做了「掃描所有 @Component/@Service/@RestController、組裝依賴關係、註冊路由」這一大串你完全沒手動寫的工作。
心得:這系列學完之後,接下來可以往哪走
四篇下來,已經有能力看懂並寫一個簡單的 Spring Boot 服務。接下來如果要往下深入,比較自然的下一步:
- Spring Data JPA 細節:
@Entity怎麼定義資料表 mapping、關聯(@OneToMany這類)怎麼寫 - 設定檔與 Profile:
application.yml,依照環境切換設定(對照這個專案的.env/.env.local) - 例外處理的框架化寫法:
@ExceptionHandler、@ControllerAdvice,統一攔截、轉換成一致的錯誤回應格式,呼應第三篇提到的「Spring 生態傾向少用 checked exception、集中處理」 - 測試:
@SpringBootTest、Mockito 怎麼配合建構子注入寫單元測試
這幾個都建立在這系列打好的基礎上(class/interface、DI、分層架構),真的要往 Spring 生態深入的話,會是很自然的下一批主題。