目錄

  1. 什麼是 HttpExchange
  2. 為什麼使用 HttpExchange
  3. 核心概念:接口 + 註解
  4. 詳細教程:一步步創建 HttpExchange 客户端
  • 步驟 1: 創建 Spring Boot 項目
  • 步驟 2: 準備一個要調用的遠程服務 (可選)
  • 步驟 3: 定義 HttpExchange 接口
  • 步驟 4: 創建配置類,生成 Bean
  • 步驟 5: 在服務中使用客户端
  • 步驟 6: 編寫 Controller 進行測試
  1. 高級用法與詳解
  • 路徑變量 (@PathVariable)
  • 請求參數 (@RequestParam)
  • 請求體 (@RequestBody)
  • 請求頭 (@RequestHeader)
  • 處理複雜響應
  • 異步支持 (CompletableFuture)
  • 動態 URL
  1. RestTemplateWebClient 的對比
  2. 總結

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(); // 等待異步結果
    }
}
運行和測試
  1. 啓動 Spring Boot 應用。
  2. 使用 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);
}

更常見的做法是直接在 RestClientbaseUrl 中配置,@HttpExchangeurl 只寫相對路徑。

錯誤處理

默認情況下,如果遠程服務返回 4xx 或 5xx 錯誤,RestClient 會拋出 HttpClientErrorExceptionHttpServerErrorException。你可以通過 RestClientdefaultStatusHandler 來統一處理這些錯誤。

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. 與 RestTemplateWebClient 的對比

特性

RestTemplate

WebClient

HttpExchange

編程模型

命令式 (模板方法)

命令式 (流式API) / 響應式

聲明式 (接口+註解)

阻塞/非阻塞

阻塞 (同步)

非阻塞 (異步)

兩者皆可 (底層由 RestClientWebClient 決定)

類型安全

較弱 (URL和參數是字符串)

較弱 (URL和參數是字符串)

(編譯時檢查)

易用性

簡單直接

學習曲線稍陡

非常簡潔,可讀性高

狀態

進入維護模式,未來可能移除

推薦的響應式客户端

推薦的聲明式客户端

底層實現

HttpClient

HttpClient (Netty/Jetty)

RestClient (默認) 或 WebClient

選擇建議:

  • 新的同步調用場景: 優先選擇 HttpExchange,它比 RestTemplate 更現代、更安全。
  • 新的異步/響應式調用場景: 優先選擇 HttpExchange (返回 Mono/Flux) 或直接使用 WebClientHttpExchange 提供了更好的抽象。
  • 維護舊項目: 如果項目大量使用 RestTemplate,可以繼續使用,但新功能建議遷移到 HttpExchangeWebClient

7. 總結

HttpExchange 是 Spring 生態中一個強大而優雅的 HTTP 客户端工具。它通過聲明式接口極大地簡化了遠程服務調用的代碼,提高了可讀性和可維護性。對於任何新的 Spring Boot 項目,當你需要調用外部 HTTP API 時,HttpExchange 都應該是你的首選方案之一。它完美地結合了 RestTemplate 的簡單性和 WebClient 的強大功能,並提供了一個更高級、更類型安全的抽象層。