iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Software Development

救救我啊我救我!CRUD 工程師的惡補日記系列 第 15

【Spring Boot】使用 RestTemplate 存取外部 API

  • 分享至 

  • xImage
  •  

身為後端工程師,開發 RESTful API 給前端呼叫是相當常見的事,然而我們有時也會想使用其他第三方 API。例如想實作展示天氣預報的功能,可使用「氣象開放資料平台」的 API。想取得外幣匯率資料,某些銀行有提供 API。或者工作時有遇到系統的某個功能,其實是被獨立做成一個服務,運行在別的 server,我們也可能需要存取它。

若要使用 Java 建立網路連線,其中一種做法是使用 java.net 套件下的 HttpURLConnection。但這會寫出繁瑣的程式碼,非常不方便。本文將介紹 Spring Boot 封裝好的 RestTemplate,它讓我們能以簡單的操作方式,發送請求與接收回應。

此篇亦轉載到個人部落格


一、準備 Spring Boot 專案

本文使用的 Java 版本為 17(zulu-17),Spring Boot 版本為 3.1.3。

筆者會使用測試程式的做法,來展示 RestTemplate 的使用方式。

public class ApplicationTests {

    private final RestTemplate restTemplate = new RestTemplate();

    // TODO
}

二、認識 Reqres 服務

為了透過 RestTemplate 來發送請求與接收回應,勢必要有一個伺服器讓我們串接。「Reqres」是一個免費的服務,它提供各種 RESTful API,且會回傳假資料。
https://ithelp.ithome.com.tw/upload/images/20230918/20131107TC0NduLNuh.jpg

從上圖中,可看到 GET https://reqres.in/api/users/2 這個 API 的用法。在串接外部服務時,我們必須注意其介面如何定義,比如 request 與 response 的欄位名稱、支援的 query string、HTTP 狀態碼等等。

三、發送 GET 請求

根據上一節的圖,我們知道該 API 的 response body,因此要先準備好對應的類別與欄位去接收。這個道理就像在 controller 設計 API,也會用專門的類別去接收 request body 一樣。

public class GetUserResponse {
    private UserResponse data;

    // getter, setter ...
}

import com.fasterxml.jackson.annotation.JsonProperty;
public class UserResponse {
    private int id;
    private String email;
    private String avatar;

    @JsonProperty("first_name")
    private String firstName;

    @JsonProperty("last_name")
    private String lastName;

    // getter, setter ...
}

若 response 的欄位名稱不喜歡,想換另一個名字,可利用 Jackson 提供的 @JsonProperty 標記來對接欄位。

下面是使用 RestTemplate 發送 GET 請求的範例程式。

@Test
public void testGetForSingleData() {
    // 發送請求並取得回應
    ResponseEntity<GetUserResponse> resEntity = restTemplate.getForEntity(
            "https://reqres.in/api/users/1",
            GetUserResponse.class
    );

    // 確認 HTTP 狀態碼
    assertEquals(HttpStatus.OK, resEntity.getStatusCode());

    // 確認 response header
    assertNotNull(resEntity.getHeaders().getContentType());
    assertEquals("application/json;charset=utf-8", resEntity.getHeaders().getContentType().toString());

    // 確認 response body
    GetUserResponse resBody = resEntity.getBody();
    assertNotNull(resBody);

    UserResponse data = resBody.getData();
    assertEquals(1, data.getId());
    assertEquals("george.bluth@reqres.in", data.getEmail());
    assertEquals("George", data.getFirstName());
    assertEquals("Bluth", data.getLastName());
    assertEquals("https://reqres.in/img/faces/1-image.jpg", data.getAvatar());
}

此處呼叫了 getForEntity 方法,並傳入兩個參數。第一個是 API 路徑的 url,第二個是 response 要轉換成的類別。

而方法的回傳值型態,是帶泛型的 ResponseEntity。它除了 body,還帶有 HTTP 狀態碼及 header 資料,在此一併做驗證。

這裡的範例是取得一筆使用者資料。至於另一支取得多筆資料的 API,筆者留到第五節再示範。

四、發送 POST 請求

首先一樣先觀察 request 與 response 的內容有哪些欄位。
https://ithelp.ithome.com.tw/upload/images/20230918/20131107B2nAMnqhf9.jpg

接著再準備對應的類別與欄位,以發送請求與接收回應。

// 請求
public class CreateUserRequest {
    private String name;
    private String job;

    public static CreateUserRequest of(String name, String job) {
        var req = new CreateUserRequest();
        req.name = name;
        req.job = job;
        return req;
    }

    // getter, setter ...
}

// 回應
import java.time.ZonedDateTime;
public class CreateUserResponse {
    private String id;
    private String name;
    private String job;
    private ZonedDateTime createdAt;

    // getter, setter ...
}

發送 POST 請求的做法與 GET 大同小異。除了有名稱類似的 postForEntity 方法可用,也能選擇下面範例的另一個 method。

@Test
public void testPostData() {
    CreateUserRequest createReq = CreateUserRequest.of("morpheus", "leader");
    CreateUserResponse createRes = restTemplate.postForObject(
            "https://reqres.in/api/users",
            createReq,
            CreateUserResponse.class
    );

    assertNotNull(createRes);
    assertEquals(createReq.getName(), createRes.getName());
    assertEquals(createReq.getJob(), createRes.getJob());
    assertNotNull(createRes.getId());
    assertNotNull(createRes.getCreatedAt());
}

此處呼叫了 postForObject 方法。與 postForEntity 的差異,在於前者只會回傳 response body 的物件,所以不會有 HTTP 狀態碼及 header 等資料。

五、搭配字串模板

(一)用於 API 路徑

關於前面兩節所介紹的 get 與 post 系列方法,還可傳入一個名為 uriVariables 的參數。它能讓我們搭配「字串模板」來組成 API 路徑。

舉例來說,取得 id 為 1 的使用者時,getForObject 方法的參數可以這麼傳入:

GetUserResponse res = restTemplate.getForObject(
        "https://reqres.in/api/users/{id}",
        GetUserResponse.class,
        Map.of("id", 1)
);

API 路徑的 url 寫了 {id} 當作預留位置(placeholder),而 Map 參數會提供對應的值。

(二)用於 query string

若想在 url 添加 query string,這種做法也是很方便的。

接下來以取得多個使用者的 API 來做示範,以下是 API 的定義。
https://ithelp.ithome.com.tw/upload/images/20230918/20131107SHlFEMXW5N.jpg

該 API 支援分頁(pagination),故可傳入 per_pagepage 的 query string。分別代表「每頁幾筆」和「第幾頁」。

根據 API response 內容,建立了對應的類別。

public class GetUserListResponse {
    private List<UserResponse> data;
    // getter, setter ...
}

而以下是 RestTemplate 的使用範例。

@Test
public void testGetForMultipleData() {
    GetUserListResponse res = restTemplate.getForObject(
            "https://reqres.in/api/users?page={page}&per_page={per_page}",
            GetUserListResponse.class,
            Map.of("page", 2, "per_page", 6)
    );
    assertNotNull(res);

    List<UserResponse> users = res.getData();
    assertEquals(6, users.size());
    assertEquals(7, users.get(0).getId());
    assertEquals(8, users.get(1).getId());
    assertEquals(9, users.get(2).getId());
    assertEquals(10, users.get(3).getId());
    assertEquals(11, users.get(4).getId());
    assertEquals(12, users.get(5).getId());
}

六、泛用的 exchange 方法

以下列出 RestTemplate 的數個 method,分別對應各種 HTTP 方法。讀者可自行探索它們接收的參數。

  • ResponseEntity<T> getForEntity
  • T getForObject
  • ResponseEntity<T> postForEntity
  • T postForObject
  • void put
  • void delete
  • T patchForObject

然而 PUT、DELETE 與 PATCH 並不如 GET 與 POST 那麼全面,畢竟無法得到 response 資訊或 body。

另外還有一個重點,那就是在發送請求時,這些 method 無法給予 request header。要知道有些外部的 API,可能會要求呼叫方在「Authorization」這個 header 攜帶 access token 之類的值,來表明自身身份。

為了因應這些問題,我們可選擇泛用的 exchange 方法。以第四節的 POST 請求為例,用 exchange 方法來寫,會類似這樣子。

@Test
public void testPostData() {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.setBearerAuth("your_access_token");

    CreateUserRequest createReq = CreateUserRequest.of("morpheus", "leader");
    HttpEntity<CreateUserRequest> httpEntity = new HttpEntity<>(createReq, headers);

    ResponseEntity<CreateUserResponse> resEntity = restTemplate.exchange(
            "https://reqres.in/api/users",
            HttpMethod.POST,
            httpEntity,
            CreateUserResponse.class
    );

    CreateUserResponse createRes = resEntity.getBody();
    // ...
}

上面的 exchange 方法參數,接收了 HTTP 方法(HttpMethod),以及由 header 與 request body 組成的 HttpEntity

七、接收 JSON array 回應

Reqres 所回傳的 response body 都是 JSON object,結構如下。

{
    "foo": "...",
    "bar": [
        {
            "...": "..."
        }
    ]
}

然而其他 API 也許會回傳 JSON array,結構如下。

[
    {
        "foo": "...",
        "bar": "..."
    },
    {
        "foo": "...",
        "bar": "..."
    }
]

針對 JSON array 的回應,使用 RestTemplate 時,response body 的類別(Class<T> responseType 參數)可使用 類別[].class 的寫法,例如 UserResponse[].class,以陣列來接收資料。

若讀者更偏好直接用 List,則可提供 ParameterizedTypeReference<T> responseType 這個參數。兩者用途相同,具體寫法如下。

List<UserResponse> users = restTemplate.exchange(
        "your_url",
        HttpMethod.GET,
        HttpEntity.EMPTY,
        new ParameterizedTypeReference<List<YourResponse>>() {}
);

ParameterizedTypeReference 能傳入一個泛型類別,而這個類別還可以繼續包泛型。如此就能直接以指定類別的 List 來接收 response body 了。

下一篇文章會分享兩個串接第三方服務的實例,出處均來自於筆者工作中遇到的需求。分別是匯率及 IP 所在地資訊的 API。

本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch22-1


今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教/images/emoticon/emoticon41.gif


上一篇
【Spring Boot】整合 FreeMarker 產生 HTML 內容
下一篇
【Spring Boot】RestTemplate 串接第三方服務實例
系列文
救救我啊我救我!CRUD 工程師的惡補日記50
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言