近幾年來,Spring Boot 是相當受歡迎的後端開發 Framework。 利用 Spring 龐大的套件庫,我們幾乎可以輕鬆地達成任何的功能。開發後的程式,利用 Spring Cloud,也可以轉換成 Microservices 架構。本章將介紹如何用 Spring Boot 建立一個後端程式,並提供 REST APIs,和串接資料庫。
Table of Contents
建立專案 – Spring Initializr
建立 Spring Boot 專案最簡單的方法,就是利用 Spring Initializr 線上工具。它的畫面如下:
- Project:選擇 Maven 或是 Gradle 專案管理工具。
- Language:選擇用 Java 或是 Kotlin 語言。
- Spring Boot:選擇最新的 Stable 版本。
- Project Metadata:主要是填入 Group、Artifact 和 Name。
- Packaging:選擇編譯後打包的方式。選用 Jar 的話,會包含 Tomcat,可以直接執行。
- Java:選擇 Java SDK 的版本。Kotlin 最終也是編譯成 Java Bytecode。
- Dependencies:點 ADD DEPENDENCIES 按鈕新增套件。我們先新增 Spring Web 套件,它可以讓專案有 REST APIs。

都選好後,點擊左下方的 GENERATE 就可以下載專案,然後用 IDE 打開。IDE 的話,免費的可以用 Eclipse,付費的推薦 IntelliJ IDEA。
建立 RESTful Web Services
HTTP GET Requests
讓我們來試著建立一個 REST API。首先,先新增 User。
package com.waynestalk.demo.domain data class User(val name: String?, var age: Int?)
新增 UserController,而它有兩個 Annotation:
@RestController:結合了@Controller和@ResponseBody這兩個 Annotations。如果 Controller 是提供 RESTful Web Service,就會用@RestController。@RequestMapping:將 Web Request 對應到某個路徑。如這邊是對應到 /users。若加在 class 上,就表示 class 裡面所有 method 都會在 /users 下面。若加在 method 上,則表示是 method 的路徑。
在 UserController 裡面,新增一個 GET Request Handler。
- @GetMapping:它對應到一個 HTTP GET Request 到這個 method。我們沒有指定子路徑,所以它會使用上層 UserController 的 /users 路徑。此外,它等同於
@RequestMapping(method = [RequestMethod.GET]),只是比較短。 - @RequestParam:如果 Request URL 是 /users?age=20,那它會解析後面的 query string。
required是指,agequery param 是否必要設定。default是指,沒設定時的預設值。 - @GetMapping(“/{name}”):所有的 /users/XXX HTTP GET Request 都會對應到這個 method,XXX 是指任何字串。
- @PathVariable:將 XXX 值會放入
name參數。
package com.waynestalk.demo.controller
import com.waynestalk.demo.domain.User
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/users")
class UserController {
private var users = listOf(
User("Jason", 20),
User("Alan", 22),
User("David", 21),
User("Monika", 20),
User("Angela", 22)
)
@GetMapping
fun getUsers(@RequestParam(required = false, defaultValue = "0") age: Int) =
if (age == 0) users else users.filter { it.age == age }
@GetMapping("/{name}")
fun getUserBy(@PathVariable name: String) =
users.find { it.name == name }
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
}執行專案後,可以用瀏覽器直接開 http://localhost:8080/users,就可以取得所有 User:
[
{
"name":"Jason",
"age":20
},
{
"name":"Alan",
"age":22
},
{
"name":"David",
"age":21
},
{
"name":"Monika",
"age":20
},
{
"name":"Angela",
"age":22
}
]也可以在命令列上,用 curl 指令發出 HTTP GET Request。
% curl --location --request GET 'http://localhost:8080/users'
[{"name":"Jason","age":20},{"name":"Alan","age":22},{"name":"David","age":21},{"name":"Monika","age":20},{"name":"Angela","age":22}]搜尋 age 是 20 的 User。
% curl --location --request GET 'http://localhost:8080/users?age=20'
[{"name":"Jason","age":20},{"name":"Monika","age":20}]取得 User Monika。
% curl --location --request GET 'http://localhost:8080/users/Monika'
{"name":"Monika","age":20}HTTP POST Requests
接下來,我們新增一個 REST API 來新增 User。在 UserController 裡新增一個 POST Request Handler。
- @PostMapping: 它等同於
@RequestMapping(method = [RequestMethod.POST]) - @RequestBody:它會將 Web Request 的 Body 轉換成
User的結構。它會根據 Web Request Headers 中的Content-Type來決定要用哪個HttpMessageConverter來解析 Body。
@RestController
@RequestMapping("/users")
class UserController {
@PostMapping
fun addUser(@RequestBody user: User): User {
if (users.find { it.name == user.name } != null)
throw ResponseStatusException(HttpStatus.CONFLICT, "Duplicated user ${user.name}")
users.add(user)
return user
}
}用 curl 指令來新增 User Sofia。
% curl --location --request POST 'http://localhost:8080/users' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Sofia",
"age": 10
}'HTTP PUT Requests
一般來說,新增東西的話會用 POST Request,而修改東西的話就會用 PUT Request。其用法大同小異。
@RestController
@RequestMapping("/users")
class UserController {
@PutMapping("/{name}")
fun modifyUser(@PathVariable name: String, @RequestBody user: User): User {
val found: User = users.find { it.name == name }
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
found.age = user.age
return found
}
}用 curl 來修改 User Monika。
% curl --location --request PUT 'http://localhost:8080/users/Monika' \
--header 'Content-Type: application/json' \
--data-raw '{
"age": 24
}'HTTP DELETE Requests
最後要介紹的 HTTP Request 就是 DELETE – 刪除。
@RestController
@RequestMapping("/users")
class UserController {
@DeleteMapping("/{name}")
fun removeUser(@PathVariable name: String): User {
val found = users.find { it.name == name }
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
users.remove(found)
return found
}
}用 curl 刪除 User Monika。
% curl --location --request DELETE 'http://localhost:8080/users/Monika'
REST API 命名規格
上面介紹了 4 種 HTTP Request。但是只用到 2 種 URI,一個是 /users,另外一個是 /users/{name}。我們可能會想說,為何不每一種 HTTP Request 有唯一的 URI 呢?像是這樣:
- GET:/users/get、/users/{name}/get
- POST:/users/add
- PUT:/users/{name}/update、/users/update 並將 name 指定在 body 中
- DELETE:/users/{name}/delete、/users/delete 並將 name 指定在 body 中
這在 REST API Naming Convention 有詳細的解說。簡短來說,REST API 要以 Resources 為主,而不是以 Actions 為主。也就是說,像 /users/Monika 這個 Resource,它提供 3 個 Actions,讓我們可以取得資料 (GET)、更新 (PUT)、和移除 (DELETE)。而 /users Resource 提供 2 個 Actions,讓我們可以取得列表和新增。
串連資料庫 – JPA
JPA 是 Java Persistence API 的縮寫。它無法直接使用,而是定義了一套 API,讓其他的 Framework 可以實作。所以用 JPA 的話,我們可以替換底層的資料庫,而不需要改動程式。
JPA 使用 Object Relational Mapping (ORM) 的架構。所謂的 ORM,簡單來說,就是資料表和資料表欄位會映射到對應的物件 (Object)。因此在使用時,開發者面對的是物件,而不是資料庫。所以開發起來就相對地輕鬆快速,而且也不太需要直接撰寫 SQL。
H2
H2 是一個 In-Memory 資料庫。在專案初期,或是在撰寫測試程式時,In-Memory 資料庫總是首選。
要使用 H2 的話,在建立專案時,引入 Spring Data JPA 和 H2 Database 套件。

加入那兩個 Dependencies 後,你會發現在 build.gradle.kts 會多出下面這幾行:
// build.gradle.kts
plugins {
kotlin("plugin.jpa") version "1.3.72"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
}引用套件後,接下來我們要修改程式,將 User 列表存入資料庫。
首先,將 User 修改成如下。裡面多了一些 Annotations。
- @Entity:設定 class 為 Entity。這樣
class User就會對應到 user 資料表。 - @Column:當 class 被設定為 Entity 時,裡面的每一個 field 都會對應到資料表的 Column。而
@Column可以對資料表 Column 做些設定。例如,這邊我們設定name的值必須唯一 –Column(unique = true)。 - @Id:設定這個 Column 是這個資料表的 Primary Key。
- @GeneratedValue:讓
@IdColumn 的值在 insert 時會自動產生。
package com.waynestalk.demo.domain
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
@Entity
data class User(
@Column(unique = true) val name: String?,
var age: Int?,
@Id @GeneratedValue var id: Long? = null)再來新增 UserRepository 來存取資料庫。UserRepository 本身是 interface,而且繼承 JpaRepository(而它是繼承 Repository)。Spring 會掃描所有繼承 Repository 的 interface,並自動產生相對應的 class。JpaRepository 已經定義了不少資料庫存取的 method,Spring 都會自動產生。
此外,JPA 有一套命名的機制,依據那套機制命名的 method,Spring 都會自動產生相對應的程式碼。就如下中,我們自定義了兩個搜尋的 method。那套命名機制在 Query Creation 有詳細的解釋。
package com.waynestalk.demo.repository
import com.waynestalk.demo.domain.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
interface UserRepository : JpaRepository<User, Long> {
fun findAllByAge(age: Int): List<User>
fun findByName(name: String): User?
}最後就是修改 UserController,讓它存取資料庫。
package com.waynestalk.demo.controller
import com.waynestalk.demo.domain.User
import com.waynestalk.demo.repository.UserRepository
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
@RestController
@RequestMapping("/users")
class UserController(private val userRepository: UserRepository) {
@GetMapping
fun getUsers(@RequestParam(required = false, defaultValue = "0") age: Int): List<User> =
if (age == 0) userRepository.findAll() else userRepository.findAllByAge(age)
@GetMapping("/{name}")
fun getUserBy(@PathVariable name: String) =
userRepository.findByName(name)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
@PostMapping
fun addUser(@RequestBody user: User) = userRepository.save(user)
@PutMapping("/{name}")
fun modifyUser(@PathVariable name: String, @RequestBody user: User): User {
val found = userRepository.findByName(name)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
if (user.age != null) {
found.age = user.age
}
userRepository.save(found)
return found
}
@DeleteMapping("/{name}")
fun removeUser(@PathVariable name: String): User {
val user = userRepository.findByName(name)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User $name not found")
userRepository.delete(user)
return user
}
}到這邊為止,程式碼已經修改完成。不過,之前我們一些預設的幾個 User 資料,現在都被移除了。為了方便測試,我們用下面的程式碼,在程式一啟動時,預先插入一些 User 到資料庫。
package com.waynestalk.demo
import com.waynestalk.demo.domain.User
import com.waynestalk.demo.repository.UserRepository
import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class UserConfiguration {
@Bean
fun initUsers(userRepository: UserRepository) = ApplicationRunner {
userRepository.saveAll(listOf(
User("Jason", 20, 1),
User("Alan", 22, 2),
User("David", 21, 3),
User("Monika", 20, 4),
User("Angela", 22, 5)
))
}
}MySQL
MySQL 是相當受歡迎的免費資料庫系統。功能強大,許多商業程式也都是使用 MySQL。一般來說,如果公司很有錢,就會使用 Oracle。而個人和小公司就會選用 MySQL。
要使用 MySQL 的話,就要加入 Spring Data JPA 和 MySQL Driver 這兩個套件。

套件引入後,build.gradle.kts 多了下面這幾行。
// build.gradle.kts
plugins {
kotlin("plugin.jpa") version "1.3.72"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("mysql:mysql-connector-java")
}最後在 application.properties 要加上 DataSource 的設定。
// application.properties spring.datasource.url=jdbc:mysql://localhost:3306/dbname?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&serverTimezone=Asia/Taipei spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.dialect.storage_engine=innodb
- spring.datasource.url:資料庫的 URL。
- useUnicode=true&characterEncoding=utf-8:設定使用 UTF-8 來編碼字元,這樣就可以支援中文了。
- autoReconnect=true:自動重新建立連線。
- serverTimezone=Asia/Taipei:設定時區。
- spring.datasource.username:帳號。
- spring.datasource.password:密碼。
- spring.datasource.driver-class-name:設定 JDBC driver。
- spring.jpa.hibernate.ddl-auto:設定 Hibernate 的 automatic schema generation。
- spring.jpa.database-platform:設定資料庫的種類。
- spring.jpa.properties.hibernate.dialect.storage_engine:當 Hibernate 自動建立 table 時,設定使用 innodb 而不是 MyISAM。
資料庫的轉換就完成了。程式碼的部分,拖 JPA 的福,不需要改動!
結語
剛剛接觸 Spring Boot 的話,一定會覺得 Spring Boot 很難。這是因為它有很多的 Annotations 要去了解,但也是因為這些 Annotations 讓 Spring Boot 的程式碼很小。例如,你不需要寫程式設定某個函式來處理某個 HTTP Request,而是透過 Annotation 宣告即可。Spring Boot 提供很多的 Annotations 來幫我們簡化這類的程式碼。熟悉後,你會覺得它相當地方便!









