近幾年來,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
是指,age
query 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:讓
@Id
Column 的值在 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 來幫我們簡化這類的程式碼。熟悉後,你會覺得它相當地方便!