【Spring Boot】第14課-使用 RestTemplate 存取外部 API
本文最後更新於:2025-05-09
身為後端工程師,開發 RESTful API 給前端呼叫是稀鬆平常的事,然而我們有時也會想使用其他第三方 API。例如想實作展示天氣預報的功能,可使用「氣象開放資料平台」的 API。想取得外幣匯率資料,某些銀行有提供 API。或者工作上有遇到系統的某個功能,是被獨立做成一個服務,運行在另一台 server,我們也可能需要存取它。
若要使用 Java 建立網路連線,其中一種做法是使用 java.net
套件下的 HttpURLConnection。但這會寫出繁瑣的程式碼,非常不方便。本文將介紹 Spring Boot 封裝好的 RestTemplate,它讓我們能夠以簡單且同步的寫法,發送請求與接收回應。
一、程式專案準備
本文會透過撰寫測試程式的方式,來展示 RestTemplate
的用法。
以下是一個測試類別,並將 RestTemplate
宣告為全域變數,方便使用。
public class ApplicationTests {
private static final RestTemplate restTemplate = new RestTemplate();
// TODO
}
RestTemplate
不需依賴於 Spring Boot 的環境,可直接使用。
二、認識 Reqres 服務
為了示範如何存取外部的 API,勢必得決定好要串接的伺服器。「Reqres」是一個免費的服務,它提供各種 RESTful API,且會回傳假資料。
從上圖中,可看到 GET https://reqres.in/api/users/2
這支 API 的用法。在串接外部服務時,我們必須注意 API 規格如何定義,包含 request 與 response 的欄位名稱、query string 與 HTTP 狀態碼等。
從 Reqres 的官方網頁繼續往下看,還能找到它的 Swagger 文件網頁。
關於 Swagger,我們在第 13 課已經認識過了,讀者可進行試用,本文就不贅述。
三、發送 GET 請求
(一)觀察 response body 結構
根據上一節的圖片,我們已經知道該 API 的 response body 結構如下:
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://contentcaddy.io?utm_source=reqres&utm_medium=json&utm_campaign=referral",
"text": "Tired of writing endless social media content? Let Content Caddy generate it for you."
}
}
這個結構是一個物件,它具有 2 個物件欄位。「data」欄位包含了使用者資料。而「support」欄位是 Reqres 網站自己的廣告訊息,本文我們會忽略它。
串接前,要先準備好對應的類別與欄位去接收。這就像在 Controller 實作 RESTful API 時,也會準備專門的類別去接收 request body 一樣。
public class SingleUserResponse {
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 ...
}
由於 Reqres 的欄位名稱,並不是我們熟悉的駝峰字,所以此處特地使用 Spring Boot 自帶的「Jackson」套件的 @JsonProperty
注解。目的是在反序列化(deserialize)的轉換過程中,將 JSON 資料與 Java 物件的欄位名稱互相對應。
(二)撰寫程式存取 API
下面是使用 RestTemplate
發送 GET 請求的範例程式。此處是取得一筆使用者資料。
class ApplicationTests {
private static RestTemplate restTemplate = new RestTemplate();;
@Test
public void testGetSingleUser() {
// 定義 API endpoint
Map<String, String> params = Map.of("id", "2");
String urlTemplate = "https://reqres.in/api/users/{id}";
// 準備 request header
HttpHeaders headers = new HttpHeaders();
headers.set("x-api-key", "reqres-free-v1");
// 包裝 request body 與 header
HttpEntity<Void> entity = new HttpEntity<>(null, headers);
// 發送 request
ResponseEntity<SingleUserResponse> responseEntity =
restTemplate.exchange(urlTemplate, HttpMethod.GET, entity, SingleUserResponse.class, params);
// 運用 response
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertEquals(MediaType.APPLICATION_JSON_UTF8, responseEntity.getHeaders().getContentType());
SingleUserResponse responseBody = responseEntity.getBody();
assertNotNull(responseBody);
UserResponse data = responseBody.getData();
assertEquals(2, data.getId());
assertEquals("janet.weaver@reqres.in", data.getEmail());
assertEquals("Janet", data.getFirstName());
assertEquals("Weaver", data.getLastName());
assertEquals("https://reqres.in/img/faces/2-image.jpg", data.getAvatar());
}
}
此處呼叫了 exchange
方法,它接收了許多參數,也有各種多載(overloading)方法。
以上面的範例程式為例,exchange
方法用到的參數如下。
參數 | 說明 |
---|---|
url |
提供 API 路徑,可以使用大括號作為「佔位符」(placeholder),供 uriVariables 填入參數。 |
method |
HTTP 方法。 |
requestEntity |
提供 HttpEntity 物件,它能包裝 request header 與 body。 |
responseType |
接收到的 JSON response body,要反序列化轉換成的 Java 類別。 |
uriVariables |
用來將參數填入到 url 的佔位符中。 |
其中,建立 HttpEntity
物件時,必須提供 request body。而 request header 則不一定要給。
HttpEntity
接收一個泛型類別,代表 request body 的型態。由於這個範例中沒有 request body,所以傳入 null,而泛型類別給予 Void。
又因為 Reqres 的 API 規定要攜帶「x-api-key」這個 request header,於是在程式中提供。
exchange
方法的回傳值型態,是帶有泛型類別的 ResponseEntity
。我們可以從中取出 response 的 HTTP 狀態碼、body 以及 header 等資料進行運用。此處單純做驗證的動作。
四、發送 POST 請求
本節要示範的是發送 POST 請求,下圖是 API 規格。
由於發出請求時會攜帶 request body,因此需準備對應的類別。
public class CreateUserRequest {
private String name;
private String job;
// getter, setter ...
}
而以下是 response body 對應的類別。
public class CreateUserResponse {
private String id;
private String name;
private String job;
private LocalDate createdAt;
// getter, setter ...
}
下面是發送 POST 請求的範例程式。
class ApplicationTests {
private static RestTemplate restTemplate;
// ...
@Test
public void testCreateUser() {
String url = "https://reqres.in/api/users";
HttpHeaders headers = new HttpHeaders();
headers.set("x-api-key", "reqres-free-v1");
// 準備 request body
CreateUserRequest request = new CreateUserRequest();
request.setName("morpheus");
request.setJob("leader");
HttpEntity<CreateUserRequest> entity = new HttpEntity<>(request, headers);
ResponseEntity<CreateUserResponse> responseEntity =
restTemplate.exchange(url, HttpMethod.POST, entity, CreateUserResponse.class);
assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
CreateUserResponse responseBody = responseEntity.getBody();
assertNotNull(responseBody);
assertEquals(request.getName(), responseBody.getName());
assertEquals(request.getJob(), responseBody.getJob());
assertNotNull(responseBody.getId());
assertNotNull(responseBody.getCreatedAt());
}
}
我們關注建立 HttpEntity
的部份即可。此處建立 request body 的物件,並傳入 HttpEntity
建構子的第一個參數,同時也給予對應的泛型類別。
最後一樣使用 RestTemplate
發出請求。
五、攜帶 query string
Reqres 也有提供另一支 API,是透過 query string 取得多筆資料。下圖是在 Swagger 文件上的規格。
這支 API 支援分頁(pagination)。其中「per_page」參數代表每頁的資料筆數,而「page」代表第幾頁。
以下是對應的 response body 類別。
public class ListUserResponse {
private int total;
@JsonProperty("total_pages")
private int totalPages;
private List<UserResponse> data;
// getter, setter ...
}
RestTemplate
並沒有提供添加 query string 的方法。若我們想串接這類 API,就得另外處理。
直覺的做法,可以是直接拼湊字串,或是利用 exchange
方法的 uriVariables
參數,將 query string 填入到 URL 的字串中。
class ApplicationTests {
private static RestTemplate restTemplate;
// ...
@Test
public void testGetManyUser() {
// 準備 query string
Map<String, String> queryStrings = new HashMap<>();
queryStrings.put("perPage", "3");
queryStrings.put("page", "2");
String urlTemplate = "https://reqres.in/api/users?per_page={perPage}&page={page}";
HttpHeaders headers = new HttpHeaders();
headers.set("x-api-key", "reqres-free-v1");
HttpEntity<Void> entity = new HttpEntity<>(null, headers);
// 發送 request
ResponseEntity<ListUserResponse> responseEntity =
restTemplate.exchange(urlTemplate, HttpMethod.GET, entity, ListUserResponse.class, queryStrings);
ListUserResponse responseBody = responseEntity.getBody();
assertNotNull(responseBody);
assertEquals(12, responseBody.getTotal());
assertEquals(4, responseBody.getTotalPages());
List<UserResponse> users = responseBody.getData();
assertEquals(3, users.size());
UserResponse user1 = users.get(0);
assertEquals(4, user1.getId());
assertEquals("Eve", user1.getFirstName());
UserResponse user2 = users.get(1);
assertEquals(5, user2.getId());
assertEquals("Charles", user2.getFirstName());
UserResponse user3 = users.get(2);
assertEquals(6, user3.getId());
assertEquals("Tracey", user3.getFirstName());
}
}
但如果 API 支援大量 query string,針對 URL 的字串模板進行 hard code,可能會讓程式碼不美觀,或者遇到複雜的情況也不容易處埋。
因此以下再提供一種更彈性的方式,來產生帶有 query string 的 URL。
@Test
public void testGetManyUser() {
Map<String, String> queryStrings = new HashMap<>();
queryStrings.put("per_page", "3");
queryStrings.put("page", "2");
// 定義 URL 網域
UriComponentsBuilder uriBuilder =
UriComponentsBuilder.fromUriString("https://reqres.in/api/users");
// 填入 query string
queryStrings.forEach(uriBuilder::queryParam);
// https://reqres.in/api/users?per_page=3&page=2
String url = uriBuilder.build().toString();
// ...
}
此處準備了裝有 query string 的 Map。接著建立 UriComponentsBuilder
物件,透過 fromUriString
方法定義 URL 的網域。
最後透過 queryParam
方法,將所有 query string 填入進去,輸出成字串即可。
六、接收陣列的 response body
RestTemplate
的 exchange
方法,有一個參數是提供 response body 要轉換成的 Java 類別。
假設我們所串接的 API,response body 的結構是 JSON 陣列,示意如下。
[
{
"id": 7,
"name": "Michael"
},
{
"id": 11,
"name": "George"
}
]
相較於前面的範例都是 JSON 物件,若遇上 JSON 陣列的 response body,我們是無法在呼叫 exchange
方法時,傳入帶泛型的 List,來指定要轉換成的 Java 類別。
解決的方式之一,是改為轉換成物件陣列,示意如下。
ResponseEntity<UserResponse[]> responseEntity = restTemplate.exchange(
url, HttpMethod.GET, entity, UserResponse[].class);
另一種方式,是改為傳入 ParameterizedTypeReference
物件,示意如下。
ResponseEntity<List<UserResponse>> responseEntity = restTemplate.exchange(
url, HttpMethod.GET, entity, new ParameterizedTypeReference<List<UserResponse>>() {});
建立 ParameterizedTypeReference
物件時,需傳入一個泛型類別。我們可以在此指定任何類別,包含一般類別,或是帶泛型的 List 或 Map 都行。
七、封裝成物件
前面在示範 RestTemplate
的用法時,都是在測試案例的程式中進行。本節讓我們封裝成一個物件,除了增加可讀性,使用上也更便利。
(一)設定共同參數
以下是一個用來封裝的類別,將 RestTemplate
宣告為全域變數。
public class ReqresClient {
private final RestTemplate restTemplate;
public ReqresClient() {
this.restTemplate = new RestTemplate();
}
}
接下來,我們可以幫 RestTemplate
本身進行一些設定。這些設定在呼叫各個 API 時都是需要的。
以下是設定 timeout 相關的參數。
public class ReqresClient {
private final RestTemplate restTemplate;
public ReqresClient() {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(10000);
factory.setReadTimeout(30000);
this.restTemplate = new RestTemplate(factory);
}
}
此處透過 SimpleClientHttpRequestFactory
物件,來提供 2 個 timeout 參數(單位為毫秒)。
- Connect Timeout:連線到伺服器超時。
- Read Timeout:已連線到伺服器,但等待回應超時。
這兩個方法也接受 Duration
物件,可讀性更佳。
public class ReqresClient {
private final RestTemplate restTemplate;
public ReqresClient() {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(10));
factory.setReadTimeout(Duration.ofSeconds(30));
this.restTemplate = new RestTemplate(factory);
}
}
另外,以下是設定發出請求時,攜帶固定的 request header。
public class ReqresClient {
private final RestTemplate restTemplate;
public ReqresClient() {
// ...
this.restTemplate
.getInterceptors()
.add((request, body, execution) -> {
request.getHeaders().add("x-api-key", "reqres-free-v1");
return execution.execute(request, body);
});
}
}
此處添加了 ClientHttpRequestInterceptor
物件(採用 Lambda 寫法)。在 RestTemplate
發出請求時,會被這個「攔截器」處理,添加我們自定義的 header。
當然,以上參數都能改寫成透過第 6 課介紹的「application.properties」配置檔來提供。但是要記得將這個類別建立成元件。
(二)對外提供方法
本文示範了 3 個呼叫 API 的情境,包含「取得一位使用者」、「取得多位使用者」與「建立使用者」。以下的程式,是將這些功能各自封裝成方法。
public class ReqresClient {
private static final String BASE_URL = "https://reqres.in/api";
private final RestTemplate restTemplate;
// ...
public Optional<UserResponse> getUserById(int id) {
Map<String, String> params = Map.of("id", String.valueOf(id));
String urlTemplate = BASE_URL + "/users/{id}";
ResponseEntity<SingleUserResponse> responseEntity =
restTemplate.exchange(urlTemplate, HttpMethod.GET, HttpEntity.EMPTY, SingleUserResponse.class, params);
return Optional.ofNullable(responseEntity)
.map(ResponseEntity::getBody)
.map(SingleUserResponse::getData);
}
public CreateUserResponse createUser(CreateUserRequest request) {
String url = BASE_URL + "/users";
HttpEntity<CreateUserRequest> entity = new HttpEntity<>(request);
ResponseEntity<CreateUserResponse> responseEntity =
restTemplate.exchange(url, HttpMethod.POST, entity, CreateUserResponse.class);
return responseEntity.getBody();
}
public ListUserResponse getUsers(Map<String, String> queryStrings) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(BASE_URL + "/users");
queryStrings.forEach(uriBuilder::queryParam);
String url = uriBuilder.build().toString();
ResponseEntity<ListUserResponse> responseEntity =
restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, ListUserResponse.class, queryStrings);
return responseEntity.getBody();
}
}
完成後,原先測試程式中使用到 RestTemplate
的地方,都能改成用這個封裝好的 client 類別來呼叫了。
本文的完成後專案,請點我。