【Spring Boot】第4課-在 Controller 接收 query string 與操作 header #2024 年更新


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」來進行操作(此處假設讀者知道用法)。

程式邏輯分為三個步驟。

  1. 根據排序的欄位,宣告 Comparator 物件,用來比較大小。
  2. 根據排序的方向,決定是否將 Comparator 調整為遞減。
  3. 取得測試資料的集合,呼叫 stream 的相關方法。將搜尋關鍵字用來篩選,再將符合的資料做排序。

完成後,可實際用 Postman 呼叫看看。

本節著重於如何在 Controller 接收 query string。至於檢查它們的值是否合法,不是此處的討論範圍。


三、接收與回傳標頭

(一)回傳 Location 標頭

第 2 課有介紹過,當 API 的功能是建立資源,那麼後端可額外回傳「Location」這個標頭(header),提供指向該資源的 endpoint。

在練習用專案中,尚有另一個 API 是 POST /products,用途是建立新的產品資料。處理方法的名稱為「createProduct」,本節就在這個 API 提供此 header。

透過 ServletUriComponentsBuilder 提供的方法,我們能一步步建構出 URI 物件。

  1. fromCurrentRequestUri:從前端呼叫當前 API 的 endpoint 為基準開始建構。例如「http://localhost:8080/products」。
  2. path:以目前建構出的結果,繼續往後添加新的路徑,亦可透過「{ }」符號挖空格(placeholder),並於後續填入值。
  3. 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 類別也有直接提供如 oknoContentnotFound 等常見的方法,可以用來替代。

以處理 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

上一篇:【Spring Boot】第3課-在 Controller 實作 RESTful API

下一篇:【Spring Boot】第5課-實作三層式架構的 Service 與 Repository

留言

  1. 你好~
    想請問一下為什麼我在使用 .equals 時會出現 「Cannot resolve method 'equals(java.lang.String)'」這個問題呢?

    感謝分享這麼詳細的教學

    回覆刪除
    回覆
    1. 謝謝~

      試試看在 IntelliJ 左上方按 Files → Invalidate Caches / Restart
      出現對話窗再按 Invalidate and Restart,等它重開
      重開後再從上方按 Build → Rebuild Project,跑完後再看有沒有問題

      刪除
  2. 你好, 請問PUT跟DELETE的路徑是不是需要改為"/products/{id}", 因為同樣對產品這個資源做刪修?

    回覆刪除
    回覆
    1. 是的,在本文第五節之前的路徑都會有「/products」,之後才會簡化成「/{id}」
      已經更正了,謝謝提醒~

      刪除
  3. 四、接收更多查詢字串
    第二段:而 desc 代表排序的順序,「asc」為遞增,「desc」為遞減。

    應為 「而 sortRule代表排序的順序」

    感謝詳盡的tutorial !

    回覆刪除

張貼留言