https://unsplash.com/photos/LmyPLbbUWhA
完成第 3 課後,相信讀者已經知道如何在 Controller 設計 API。本文將繼續介紹其他可以實作的細節。
首先是接收查詢字串(query string),進行條件篩選與排序,回傳多筆資料。接著是標頭(header)的處理,包含接收指定名稱的 request header,以及產生 URI 路徑,在建立資源後提供 Location。最後提供幾項實用的技巧,讓 Controller 的程式碼更簡潔。
本文的練習用專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch3
一、範例專案介紹
首先回顧一下練習用專案中的測試資料和 Controller。
以下的自訂類別,描述了產品的編號、名稱與價格,會作為測試資料。
以下是 Controller,由於沒有串接真實的 DB,因此將測試資料存放在 Java 的 Map 資料結構中。
二、接收查詢字串
(一)範例說明
延續練習用專案,本節讓我們實作新的 API。它能接收端點(endpoint)上的查詢字串(query string),對測試資料 Product 進行搜尋與排序。
筆者設計了三個查詢字串。
- searchKey:搜尋的關鍵字。本文支援搜尋「id」與「name」欄位。
- sortField:用來排序的欄位。本文支援「name」與「price」。
- sortDir:排序的方向。僅支援「asc」(遞增)與「desc」(遞減)。
舉例來說,若想以「manage」關鍵字搜尋,並以價格遞增排序,則 endpoint 寫法為:
GET /products?searchKey=manage&sortField=price&sortDir=asc。
(二)宣告 API
了解需求後,就能開始實作了。以下是在 Controller 先宣告 API 的處理方法。
這個方法的三個參數,分別對應到上述的搜尋關鍵字、排序欄位與方向,且都冠上了 @RequestParam 注解。
該注解又可再傳入多個參數,用途說明如下。
- value:指定要接收 endpoint 上的哪一個 query string。好處是方法參數與 query string 可分開取名,不必使用相同的名稱。否則預設將會與方法參數一樣。
- required:設定該 query string 是否為必填。預設為 true,若前端未提供,後端會自動回應 400 狀態碼(Bad Request)。
- defaultValue:若前端未提供 query string,可在此定義預設值。
最後,由於這支 API 會回傳多筆資料,而且可能有順序性,所以回傳值的 ResponseEntity,其泛型類別給予的是 List。
(三)實作程式邏輯
本文的範例程式並未串接真實的 DB,故透過 Java 的「stream」來進行操作(此處假設讀者知道用法)。
程式邏輯分為三個步驟。
- 根據排序的欄位,宣告 Comparator 物件,用來比較大小。
- 根據排序的方向,決定是否將 Comparator 調整為遞減。
- 取得測試資料的集合,呼叫 stream 的相關方法。將搜尋關鍵字用來篩選,再將符合的資料做排序。
完成後,可實際用 Postman 呼叫看看。
本節著重於如何在 Controller 接收 query string。至於檢查它們的值是否合法,不是此處的討論範圍。
三、接收與回傳標頭
(一)回傳 Location 標頭
在第 2 課有介紹過,當 API 的功能是建立資源,那麼後端可額外回傳「Location」這個標頭(header),提供指向該資源的 endpoint。
在練習用專案中,尚有另一個 API 是 POST /products,用途是建立新的產品資料。處理方法的名稱為「createProduct」,本節就在這個 API 提供此 header。
透過 ServletUriComponentsBuilder 提供的方法,我們能一步步建構出 URI 物件。
- fromCurrentRequestUri:從前端呼叫當前 API 的 endpoint 為基準開始建構。例如「http://localhost:8080/products」。
- path:以目前建構出的結果,繼續往後添加新的路徑,亦可透過「{ }」符號挖空格(placeholder),並於後續填入值。
- build:傳入 Map,將值填入上述的 placeholder 中,並回傳建立好的 URI 物件。
準備好 URI 後,請建立 HttpHeaders 物件,添加 Location 的值。HttpHeaders 提供一系列的「setXXX」方法,可以很直覺地加上資料。最後將其附帶於 ResponseEntity 中。
以下是 Postman 的操作結果,可看見 response header 確實有出現「Location」。
(二)接收標頭
Controller 除了可以回傳 header,當然也能接收。然而在本文範例 API 的情境,若刻意加入接收 header 的環節,看起來會比較違和。所以在此只簡單地示範。
以下的範例是接收一個叫做「If-Modified-Since」的 header。
這裡使用了 @RequestHeader 注解,並於注解的參數提供要接收 header 的名稱。
四、簡化 Controller 的程式
完成第 3、4 課後,讀者在 Controller 將擁有五支 RESTful API。本節將補充一些寫法,讓程式能稍微簡潔好讀一些。
(一)套用 Endpoint 前綴
這些跟產品有關的 API,在 @GetMapping、@PostMapping 等注解中,其 endpoint 的開頭都要寫上「/products」,感覺重複性有點高。
我們可以在 Controller 類別冠上 @RequestMapping 注解,定義路徑的前綴,之後底下的 endpoint 都會掛上它。
(二)選擇 HTTP 狀態碼
先前提供 HTTP 狀態碼的方式,是呼叫 ResponseEntity.status 方法,傳入 HttpStatus 這個列舉(enum)物件。
其實 ResponseEntity 類別也有直接提供如 ok、noContent、notFound 等常見的方法,可以用來替代。
以處理 GET 與 POST 請求的方法為例,將程式調整成如下。
其中 ok 方法甚至可以直接傳入 payload,而 created 方法可傳入 URI,增添了便利性。
(三)封裝多個查詢字串
在第二節的實作的 GET /products 這支 API,若開放更多 query string 傳進來,將會使程式碼太冗長。畢竟每個 @RequestParam 注解,都能傳入好幾個參數。
針對這個問題,我們可準備自定義的類別,並搭配一個叫做 @ModelAttribute 的注解,將 query string 的值自動填充到該類別的物件中。就像接收 request body 那樣。
以下是用來接收 query string 的類別之定義。
以下是調整過後的 API 處理方法。
透過 @ModelAttribute 注解,在此將 query string 都收集起來,之後再分別取出做運用。
我們在 Controller 寫了好幾支程式,內容也越來越龐大。然而有些地方其實並非 Controller 的職責,尤其是直接存取 Map 中測試資料的部份。第 5 課將介紹「三層式架構」,將不同用途的程式碼片段,分開撰寫在其他地方。
本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch4
你好~
回覆刪除想請問一下為什麼我在使用 .equals 時會出現 「Cannot resolve method 'equals(java.lang.String)'」這個問題呢?
感謝分享這麼詳細的教學
謝謝~
刪除試試看在 IntelliJ 左上方按 Files → Invalidate Caches / Restart
出現對話窗再按 Invalidate and Restart,等它重開
重開後再從上方按 Build → Rebuild Project,跑完後再看有沒有問題
你好, 請問PUT跟DELETE的路徑是不是需要改為"/products/{id}", 因為同樣對產品這個資源做刪修?
回覆刪除是的,在本文第五節之前的路徑都會有「/products」,之後才會簡化成「/{id}」
刪除已經更正了,謝謝提醒~
四、接收更多查詢字串
回覆刪除第二段:而 desc 代表排序的順序,「asc」為遞增,「desc」為遞減。
應為 「而 sortRule代表排序的順序」
感謝詳盡的tutorial !
已經更新了,謝謝 🙂
刪除