目錄
- 什麼是
HttpExchange? - 為什麼使用
HttpExchange? - 核心概念:接口 + 註解
- 詳細教程:一步步創建
HttpExchange客户端
- 步驟 1: 創建 Spring Boot 項目
- 步驟 2: 準備一個要調用的遠程服務 (可選)
- 步驟 3: 定義
HttpExchange接口 - 步驟 4: 創建配置類,生成 Bean
- 步驟 5: 在服務中使用客户端
- 步驟 6: 編寫 Controller 進行測試
- 高級用法與詳解
- 路徑變量 (
@PathVariable) - 請求參數 (
@RequestParam) - 請求體 (
@RequestBody) - 請求頭 (
@RequestHeader) - 處理複雜響應
- 異步支持 (
CompletableFuture) - 動態 URL
- 與
RestTemplate和WebClient的對比 - 總結
1. 什麼是 HttpExchange?
HttpExchange 是一個用於聲明式 HTTP 客户端的註解。你只需要定義一個 Java 接口,並用 @HttpExchange 及其相關注解(如 @GetExchange, @PostExchange)來描述 HTTP 請求的細節(URL、方法、參數等)。Spring 會在運行時自動為你創建這個接口的實現類,你就可以像調用本地方法一樣發起遠程 HTTP 請求。
它受到了 Feign、Retrofit 等庫的啓發,並將其思想無縫集成到了 Spring 生態中。
2. 為什麼使用 HttpExchange?
- 類型安全:在編譯時就能檢查 URL 路徑、參數類型是否匹配。
- 簡潔易讀:將 HTTP 調用的邏輯從業務代碼中分離出來,接口定義即文檔。
- 易於測試:可以輕鬆地為接口創建 Mock 實現,進行單元測試。
- 與 Spring 生態深度集成:默認使用
RestClient(底層基於HttpClient)作為實現,支持HttpMessageConverters、攔截器等。 - 支持響應式:可以返回
Mono/Flux(WebFlux) 或CompletableFuture,實現異步非阻塞調用。
3. 核心概念:接口 + 註解
核心思想是:定義一個接口,用註解描述 HTTP 請求。
@HttpExchange:用在類或接口上,為所有方法提供一個公共的基礎 URL。@GetExchange:用於方法,聲明一個 HTTP GET 請求。@PostExchange:用於方法,聲明一個 HTTP POST 請求。@PutExchange:用於方法,聲明一個 HTTP PUT 請求。@DeleteExchange:用於方法,聲明一個 HTTP DELETE 請求。@PatchExchange:用於方法,聲明一個 HTTP PATCH 請求。@PathVariable:綁定 URL 路徑中的變量(如/users/{id})。@RequestParam:綁定查詢參數(如?name=john)。@RequestHeader:綁定請求頭。@RequestBody:綁定請求體。
4. 詳細教程:一步步創建 HttpExchange 客户端
假設我們要調用一個外部的用户服務 API。
步驟 1: 創建 Spring Boot 項目
使用 Spring Initializr 創建一個新項目。
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x (或任何 3.x+ 版本)
- Dependencies:
Spring Web
步驟 2: 準備一個要調用的遠程服務 (可選)
為了方便演示,我們可以在同一個 Spring Boot 應用中創建一個 Controller,模擬我們要調用的遠程服務。
RemoteUserController.java
package com.example.httpexchangedemo.controller;
import com.example.httpexchangedemo.model.User;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
@RestController
@RequestMapping("/api/remote-users")
public class RemoteUserController {
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
System.out.println("Remote service: Received request for user id " + id);
// 模擬從數據庫獲取數據
return new User(id, "User-" + id, "user" + id + "@example.com");
}
@GetMapping
public List<User> getAllUsers() {
System.out.println("Remote service: Received request for all users");
return Collections.singletonList(new User(1L, "John Doe", "john.doe@example.com"));
}
@PostMapping
public User createUser(@RequestBody User user) {
System.out.println("Remote service: Received request to create user " + user);
// 模擬創建用户並返回帶ID的用户
user.setId(100L);
return user;
}
}
User.java (模型類)
package com.example.httpexchangedemo.model;
public record User(Long id, String name, String email) {}
注意: 這裏使用了 Java 16+ 的
record,它非常適合作為數據傳輸對象。如果你使用舊版 Java,可以創建一個標準的 POJO。
步驟 3: 定義 HttpExchange 接口
這是最核心的一步。我們創建一個接口來描述對 /api/remote-users 的所有調用。
UserClient.java
package com.example.httpexchangedemo.client;
import com.example.httpexchangedemo.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import java.util.List;
import java.util.concurrent.CompletableFuture;
// @HttpExchange 定義了整個接口的基礎路徑
@HttpExchange(url = "/api/remote-users", accept = "application/json")
public interface UserClient {
// GET /api/remote-users/{id}
@GetExchange("/{id}")
User getUserById(@PathVariable("id") Long id);
// GET /api/remote-users?id=1
@GetExchange
User getUserByIdWithParam(@RequestParam("id") Long id);
// GET /api/remote-users
@GetExchange
List<User> getAllUsers();
// POST /api/remote-users
@PostExchange
ResponseEntity<User> createUser(@RequestBody User user, @RequestHeader("X-Request-Source") String source);
// 異步調用示例
// GET /api/remote-users/{id}
@GetExchange("/{id}")
CompletableFuture<User> getUserByIdAsync(@PathVariable Long id);
}
步驟 4: 創建配置類,生成 Bean
我們需要告訴 Spring 如何為我們的 UserClient 接口創建實現。這通常通過一個 @Configuration 類來完成。
ClientConfig.java
package com.example.httpexchangedemo.config;
import com.example.httpexchangedemo.client.UserClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class ClientConfig {
@Bean
public UserClient userClient() {
// 1. 創建 RestClient 實例,並配置基礎URL
// 這裏我們調用本地的服務,所以用 localhost:8080
RestClient restClient = RestClient.builder()
.baseUrl("http://localhost:8080")
.build();
// 2. 使用 HttpServiceProxyFactory 創建代理工廠
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(restClient).build();
// 3. 為 UserClient 接口創建代理對象
return factory.createClient(UserClient.class);
}
}
這個配置是關鍵。它將 UserClient 接口與 RestClient(Spring 6.1 推薦的同步客户端)綁定,並註冊為一個 Spring Bean,這樣我們就可以在其他地方通過 @Autowired 注入並使用了。
步驟 5: 在服務中使用客户端
現在,我們可以在任何其他 Spring Bean(比如一個 @Service)中注入並使用 UserClient。
UserService.java
package com.example.httpexchangedemo.service;
import com.example.httpexchangedemo.client.UserClient;
import com.example.httpexchangedemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
public class UserService {
private final UserClient userClient;
@Autowired
public UserService(UserClient userClient) {
this.userClient = userClient;
}
public User findUserById(Long id) {
System.out.println("Calling remote service to get user with id: " + id);
return userClient.getUserById(id);
}
public List<User> findAllUsers() {
System.out.println("Calling remote service to get all users");
return userClient.getAllUsers();
}
public User createNewUser(User user) {
System.out.println("Calling remote service to create a new user");
return userClient.createUser(user, "My-App").getBody(); // 從 ResponseEntity 中獲取 Body
}
public CompletableFuture<User> findUserByIdAsync(Long id) {
System.out.println("Calling remote service ASYNC to get user with id: " + id);
return userClient.getUserByIdAsync(id);
}
}
步驟 6: 編寫 Controller 進行測試
最後,創建一個 Controller 來觸發我們的 UserService,從而驗證 HttpExchange 是否正常工作。
TestController.java
package com.example.httpexchangedemo.controller;
import com.example.httpexchangedemo.model.User;
import com.example.httpexchangedemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/api/local")
public class TestController {
private final UserService userService;
@Autowired
public TestController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findUserById(id);
}
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.findAllUsers();
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.createNewUser(user);
}
@GetMapping("/users-async/{id}")
public User getUserAsync(@PathVariable Long id) throws ExecutionException, InterruptedException {
return userService.findUserByIdAsync(id).get(); // 等待異步結果
}
}
運行和測試
- 啓動 Spring Boot 應用。
- 使用
curl或 Postman 測試:
- 獲取單個用户 (同步):
curl http://localhost:8080/api/local/users/1
預期輸出: {"id":1,"name":"User-1","email":"user1@example.com"}
- 獲取所有用户:
curl http://localhost:8080/api/local/users
預期輸出: [{"id":1,"name":"John Doe","email":"john.doe@example.com"}]
- 創建用户:
curl -X POST http://localhost:8080/api/local/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@test.com"}'
預期輸出: {"id":100,"name":"Alice","email":"alice@test.com"}
- 獲取單個用户 (異步):
curl http://localhost:8080/api/local/users-async/2
預期輸出: {"id":2,"name":"User-2","email":"user2@example.com"}
5. 高級用法與詳解
動態 URL
如果基礎 URL 是動態的(例如,從配置中心讀取),可以在 @HttpExchange 中使用佔位符,並在 RestClient 中提供變量。
// 接口
@HttpExchange(url = "${api.base-url}/users")
public interface UserClient { ... }
// 配置
@Bean
public UserClient userClient(@Value("${api.base-url}") String baseUrl) {
RestClient restClient = RestClient.builder()
.baseUrl("http://localhost:8080") // RestClient 的 baseUrl 仍然需要
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderForRestClient()
.baseUrl(baseUrl) // 這裏可以覆蓋
.build();
return factory.createClient(UserClient.class);
}
更常見的做法是直接在 RestClient 的 baseUrl 中配置,@HttpExchange 的 url 只寫相對路徑。
錯誤處理
默認情況下,如果遠程服務返回 4xx 或 5xx 錯誤,RestClient 會拋出 HttpClientErrorException 或 HttpServerErrorException。你可以通過 RestClient 的 defaultStatusHandler 來統一處理這些錯誤。
RestClient restClient = RestClient.builder()
.baseUrl("http://localhost:8080")
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
// 自定義錯誤處理邏輯,例如記錄日誌或拋出自定義異常
System.err.println("Error from remote service: " + response.getStatusCode());
throw new MyCustomApiException("API call failed with status: " + response.getStatusCode());
})
.build();
6. 與 RestTemplate 和 WebClient 的對比
|
特性
|
|
|
|
|
編程模型 |
命令式 (模板方法)
|
命令式 (流式API) / 響應式
|
聲明式 (接口+註解) |
|
阻塞/非阻塞 |
阻塞 (同步)
|
非阻塞 (異步) |
兩者皆可 (底層由 |
|
類型安全 |
較弱 (URL和參數是字符串)
|
較弱 (URL和參數是字符串)
|
強 (編譯時檢查) |
|
易用性 |
簡單直接
|
學習曲線稍陡
|
非常簡潔,可讀性高 |
|
狀態 |
進入維護模式,未來可能移除
|
推薦的響應式客户端 |
推薦的聲明式客户端 |
|
底層實現 |
|
|
|
選擇建議:
- 新的同步調用場景: 優先選擇
HttpExchange,它比RestTemplate更現代、更安全。 - 新的異步/響應式調用場景: 優先選擇
HttpExchange(返回Mono/Flux) 或直接使用WebClient。HttpExchange提供了更好的抽象。 - 維護舊項目: 如果項目大量使用
RestTemplate,可以繼續使用,但新功能建議遷移到HttpExchange或WebClient。
7. 總結
HttpExchange 是 Spring 生態中一個強大而優雅的 HTTP 客户端工具。它通過聲明式接口極大地簡化了遠程服務調用的代碼,提高了可讀性和可維護性。對於任何新的 Spring Boot 項目,當你需要調用外部 HTTP API 時,HttpExchange 都應該是你的首選方案之一。它完美地結合了 RestTemplate 的簡單性和 WebClient 的強大功能,並提供了一個更高級、更類型安全的抽象層。