SpringBoot手寫RestTemplate攔截器鏈,掌控HTTP請求實(shí)踐

01 前言
在項(xiàng)目開發(fā)中,RestTemplate作為Spring提供的HTTP客戶端工具,經(jīng)常用于訪問內(nèi)部或三方服務(wù)。
但實(shí)際開發(fā)時,我們常常需要對請求做統(tǒng)一處理,比如添加認(rèn)證Token、記錄請求日志、設(shè)置超時時間、實(shí)現(xiàn)失敗重試等。
要是每個接口調(diào)用都手動處理這些邏輯,不僅代碼冗余,還容易出錯。
02 為什么需要攔截器鏈?
先看一個場景:假設(shè)支付服務(wù)需要調(diào)用第三方支付接口,每次請求都得做一系列操作。
如果沒有攔截器,代碼會寫成這樣:
public String createOrder(String orderId) {
// 創(chuàng)建RestTemplate實(shí)例
RestTemplate restTemplate = new RestTemplate();
// 設(shè)置請求超時時間
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 連接超時3秒
factory.setReadTimeout(3000); // 讀取超時3秒
restTemplate.setRequestFactory(factory);
// 設(shè)置請求頭(添加認(rèn)證信息)
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getToken()); // 拼接認(rèn)證Token
HttpEntity<String> entity = new HttpEntity<>(headers);
// 記錄請求日志
log.info("請求支付接口: {}", orderId);
try {
// 發(fā)送請求
String result = restTemplate.postForObject(PAY_URL, entity, String.class);
log.info("請求成功: {}", result);
return result;
} catch (Exception e) {
log.error("請求失敗", e);
// 失敗重試邏輯
for (int i = 0; i < 2; i++) {
try {
return restTemplate.postForObject(PAY_URL, entity, String.class);
} catch (Exception ex) { }
}
throw e;
}
}
這段代碼的問題很明顯:重復(fù)邏輯散落在各個接口調(diào)用中,維護(hù)成本極高。
如果有10個接口需要調(diào)用第三方服務(wù),就要寫10遍相同的代碼。
而攔截器鏈能把這些通用邏輯抽離成獨(dú)立組件,按順序串聯(lián)執(zhí)行,實(shí)現(xiàn)"一次定義,處處復(fù)用"。
核心調(diào)用代碼可以簡化為:
return restTemplate.postForObject(PAY_URL, entity, String.class);
03 RestTemplate攔截器基礎(chǔ)
Spring提供了ClientHttpRequestInterceptor接口,所有自定義攔截器都要實(shí)現(xiàn)它。
public interface ClientHttpRequestInterceptor {
// 攔截方法:處理請求后通過execution繼續(xù)執(zhí)行調(diào)用鏈
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}
攔截器工作流程
我們先實(shí)現(xiàn)一個打印請求響應(yīng)日志的攔截器,這是項(xiàng)目中最常用的功能:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j // 簡化日志輸出
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 記錄請求開始時間
long start = System.currentTimeMillis();
// 打印請求信息
log.info("===== 請求開始 =====");
log.info("URL: {}", request.getURI()); // 請求地址
log.info("Method: {}", request.getMethod()); // 請求方法(GET/POST等)
log.info("Headers: {}", request.getHeaders()); // 請求頭
log.info("Body: {}", new String(body, StandardCharsets.UTF_8)); // 請求體
// 執(zhí)行后續(xù)攔截器或?qū)嶋H請求
ClientHttpResponse response = execution.execute(request, body);
// 讀取響應(yīng)體(注意:默認(rèn)流只能讀一次)
String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
// 打印響應(yīng)信息
log.info("===== 請求結(jié)束 =====");
log.info("Status: {}", response.getStatusCode()); // 響應(yīng)狀態(tài)碼
log.info("Headers: {}", response.getHeaders()); // 響應(yīng)頭
log.info("Body: {}", responseBody); // 響應(yīng)體
log.info("耗時: {}ms", System.currentTimeMillis() - start); // 計算請求耗時
return response;
}
}
關(guān)鍵點(diǎn):
- execution.execute(…)是調(diào)用鏈的核心,必須調(diào)用才能繼續(xù)執(zhí)行后續(xù)攔截器或發(fā)送實(shí)際請求。
- 響應(yīng)流response.getBody()默認(rèn)只能讀取一次,如需多次處理需緩存(后面會講解決方案)。
04 攔截器鏈實(shí)戰(zhàn):從單攔截器到多攔截器協(xié)同
SpringBoot中通過RestTemplate.setInterceptors()注冊多個攔截器,形成調(diào)用鏈。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@Configuration // 標(biāo)記為配置類,讓Spring掃描加載
public class RestTemplateConfig {
@Bean // 將RestTemplate實(shí)例注入Spring容器
public RestTemplate customRestTemplate() {
// 創(chuàng)建攔截器列表
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new AuthInterceptor()); // 1. 認(rèn)證攔截器
interceptors.add(new LoggingInterceptor()); // 2. 日志攔截器
interceptors.add(new RetryInterceptor(2)); // 3. 重試攔截器(最多重試2次)
// 創(chuàng)建RestTemplate并設(shè)置攔截器
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(interceptors);
// 配置請求工廠(支持響應(yīng)流緩存)
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 連接超時3秒
factory.setReadTimeout(3000); // 讀取超時3秒
// 用BufferingClientHttpRequestFactory包裝,解決響應(yīng)流只能讀一次的問題
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(factory));
return restTemplate;
}
}
攔截器執(zhí)行順序
按添加順序執(zhí)行(像排隊一樣),上述示例的執(zhí)行流程是:
AuthInterceptor → LoggingInterceptor → RetryInterceptor → 實(shí)際請求 → RetryInterceptor → LoggingInterceptor → AuthInterceptor
認(rèn)證攔截器示例
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
public class AuthInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 獲取認(rèn)證Token(實(shí)際項(xiàng)目中可能從緩存或配置中心獲?。?
String token = getToken();
// 往請求頭添加認(rèn)證信息
HttpHeaders headers = request.getHeaders();
headers.set("Authorization", "Bearer " + token); // 標(biāo)準(zhǔn)Bearer認(rèn)證格式
// 繼續(xù)執(zhí)行后續(xù)攔截器
return execution.execute(request, body);
}
// 模擬獲取Token的方法
private String getToken() {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 實(shí)際為真實(shí)Token字符串
}
}
重試攔截器示例
package com.example.rest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.HttpStatusCodeException;
import java.io.IOException;
@Slf4j
public class RetryInterceptor implements ClientHttpRequestInterceptor {
private final int maxRetries; // 最大重試次數(shù)
// 構(gòu)造方法指定最大重試次數(shù)
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
int retryCount = 0; // 當(dāng)前重試次數(shù)
while (true) {
try {
// 執(zhí)行請求,若成功直接返回響應(yīng)
return execution.execute(request, body);
} catch (HttpStatusCodeException e) {
// 處理HTTP狀態(tài)碼異常(如5xx服務(wù)器錯誤)
if (e.getStatusCode().is5xxServerError() && retryCount < maxRetries) {
retryCount++;
log.warn("服務(wù)器錯誤,開始第{}次重試,狀態(tài)碼:{}", retryCount, e.getStatusCode());
continue; // 繼續(xù)重試
}
// 不滿足重試條件,拋出異常
throw e;
} catch (IOException e) {
// 處理網(wǎng)絡(luò)異常(如連接超時)
if (retryCount < maxRetries) {
retryCount++;
log.warn("網(wǎng)絡(luò)異常,開始第{}次重試", retryCount, e);
continue; // 繼續(xù)重試
}
// 重試次數(shù)耗盡,拋出異常
throw e;
}
}
}
}
05 實(shí)戰(zhàn)踩坑指南
響應(yīng)流讀取問題
現(xiàn)象:日志攔截器讀取響應(yīng)體后,后續(xù)攔截器再讀會讀取到空數(shù)據(jù)。
原因:響應(yīng)流默認(rèn)是一次性的,讀完就關(guān)閉了。
解決方案:用BufferingClientHttpRequestFactory包裝請求工廠,緩存響應(yīng)流:
// 創(chuàng)建基礎(chǔ)請求工廠 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 包裝成支持響應(yīng)緩存的工廠 BufferingClientHttpRequestFactory bufferingFactory = new BufferingClientHttpRequestFactory(factory); // 設(shè)置到RestTemplate restTemplate.setRequestFactory(bufferingFactory); (上述代碼已在04節(jié)的配置類中體現(xiàn))
攔截器順序問題
反例:把LoggingInterceptor放在AuthInterceptor前面,日志中會看不到Authorization頭。
因?yàn)槿罩緮r截器先執(zhí)行時,認(rèn)證攔截器還沒添加認(rèn)證頭。
正確順序(按職責(zé)劃分):
- 前置處理攔截器:認(rèn)證、加密、參數(shù)修改
- 日志攔截器:記錄完整請求(包含前置處理的結(jié)果)
- 重試/降級攔截器:處理異常情況(放在最后,確保重試時能重新執(zhí)行所有前置邏輯)
線程安全問題
RestTemplate是線程安全的,但攔截器若有成員變量,需確保線程安全!
錯誤示例:
public class BadInterceptor implements ClientHttpRequestInterceptor {
private int count = 0; // 非線程安全的成員變量
@Override
public ClientHttpResponse intercept(...) {
count++; // 多線程環(huán)境下會出現(xiàn)計數(shù)錯誤
}
}
解決方案:
- 攔截器避免定義可變成員變量
- 必須使用時,用ThreadLocal隔離線程狀態(tài)(每個線程獨(dú)立維護(hù)變量副本)
06 測試示例
package com.example.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class TestController {
// 注入自定義的RestTemplate(已包含攔截器鏈)
@Autowired
private RestTemplate customRestTemplate;
// 測試接口:調(diào)用第三方服務(wù)
@GetMapping("/call-third")
public String callThirdApi() {
// 直接調(diào)用,攔截器會自動處理認(rèn)證、日志、重試等邏輯
return customRestTemplate.getForObject("http://localhost:8080/mock-third-api", String.class);
}
// 模擬第三方接口
@GetMapping("/mock-third-api")
public String mockThirdApi() {
return "hello from third party";
}
}
所需依賴(只需基礎(chǔ)的Spring Boot Starter Web):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
07 總結(jié)與擴(kuò)展
通過自定義RestTemplate的攔截器鏈,我們可以將請求處理的通用邏輯(認(rèn)證、日志、重試等)抽離成獨(dú)立組件,實(shí)現(xiàn)代碼復(fù)用和統(tǒng)一維護(hù)。
- 攔截器鏈順序:按"前置處理→日志→重試"順序注冊,確保功能正確。
- 響應(yīng)流處理:使用BufferingClientHttpRequestFactory解決流只能讀取一次問題。
- 線程安全:攔截器避免定義可變成員變量,必要時使用ThreadLocal。
- 異常處理:重試攔截器需明確重試條件(如只對5xx錯誤重試,避免對4xx客戶端錯誤重試)。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- SpringBoot3使用RestTemplate請求接口忽略SSL證書的問題解決
- SpringBoot下使用RestTemplate實(shí)現(xiàn)遠(yuǎn)程服務(wù)調(diào)用的詳細(xì)過程
- SpringBoot自定義RestTemplate的攔截器鏈的實(shí)戰(zhàn)指南
- SpringBoot利用RestTemplate實(shí)現(xiàn)反向代理
- Springboot之restTemplate配置及使用方式
- SpringBoot使用RestTemplate如何通過http請求將文件下載到本地
- SpringBoot3 RestTemplate配置與使用詳解
- SpringBoot中restTemplate請求存在亂碼問題的解決方法
相關(guān)文章
IDEA中啟動多個SpringBoot服務(wù)的實(shí)現(xiàn)示例
本文主要介紹了IDEA中啟動多個SpringBoot服務(wù)的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08
IDEA下使用Spring Boot熱加載的實(shí)現(xiàn)
本文主要介紹了IDEA下使用Spring Boot熱加載的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Spring?Boot如何接入Security權(quán)限認(rèn)證服務(wù)
Spring Security?是一個高度可定制的身份驗(yàn)證和訪問控制的框架,提供了完善的認(rèn)證機(jī)制和方法級的授權(quán)功能,本文通過案例將Spring Security整合到SpringBoot中,要實(shí)現(xiàn)的功能就是在認(rèn)證服務(wù)器上登錄,然后獲取Token,再訪問資源服務(wù)器中的資源,感興趣的朋友一起看看吧2024-07-07
關(guān)于Rabbitmq死信隊列及延時隊列的實(shí)現(xiàn)
這篇文章主要介紹了關(guān)于Rabbitmq死信隊列及延時隊列的實(shí)現(xiàn),TTL就是消息或者隊列的過期功能,當(dāng)消息過期就會進(jìn)到死信隊列,死信隊列和普通隊列沒啥區(qū)別,然后我們只需要配置一個消費(fèi)者來消費(fèi)死信隊列里面的消息就可以了,需要的朋友可以參考下2023-08-08
Java中編譯期異常和運(yùn)行期異常的區(qū)別解析
Java中的異常分為運(yùn)行期異常(RuntimeException)和編譯期異常(CheckedException),前者不強(qiáng)制處理,后者必須顯式處理,本文介紹Java中編譯期異常和運(yùn)行期異常的區(qū)別,感興趣的朋友一起看看吧2025-02-02
SpringBoot項(xiàng)目從18.18M瘦身到0.18M的實(shí)現(xiàn)
本文主要介紹了SpringBoot項(xiàng)目從18.18M瘦身到0.18M的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
Spring Boot LocalDateTime格式化處理的示例詳解
這篇文章主要介紹了Spring Boot LocalDateTime格式化處理的示例詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10
springboot集成kafka消費(fèi)手動啟動停止操作
這篇文章主要介紹了springboot集成kafka消費(fèi)手動啟動停止操作,本文給大家介紹項(xiàng)目場景及解決分析,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09

