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

01 前言
在項(xiàng)目開發(fā)中,RestTemplate作為Spring提供的HTTP客戶端工具,經(jīng)常用于訪問內(nèi)部或三方服務(wù)。
但實(shí)際開發(fā)時(shí),我們常常需要對(duì)請(qǐng)求做統(tǒng)一處理,比如添加認(rèn)證Token、記錄請(qǐng)求日志、設(shè)置超時(shí)時(shí)間、實(shí)現(xiàn)失敗重試等。
要是每個(gè)接口調(diào)用都手動(dòng)處理這些邏輯,不僅代碼冗余,還容易出錯(cuò)。
02 為什么需要攔截器鏈?
先看一個(gè)場(chǎng)景:假設(shè)支付服務(wù)需要調(diào)用第三方支付接口,每次請(qǐng)求都得做一系列操作。
如果沒有攔截器,代碼會(huì)寫成這樣:
public String createOrder(String orderId) {
// 創(chuàng)建RestTemplate實(shí)例
RestTemplate restTemplate = new RestTemplate();
// 設(shè)置請(qǐng)求超時(shí)時(shí)間
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 連接超時(shí)3秒
factory.setReadTimeout(3000); // 讀取超時(shí)3秒
restTemplate.setRequestFactory(factory);
// 設(shè)置請(qǐng)求頭(添加認(rèn)證信息)
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getToken()); // 拼接認(rèn)證Token
HttpEntity<String> entity = new HttpEntity<>(headers);
// 記錄請(qǐng)求日志
log.info("請(qǐng)求支付接口: {}", orderId);
try {
// 發(fā)送請(qǐng)求
String result = restTemplate.postForObject(PAY_URL, entity, String.class);
log.info("請(qǐng)求成功: {}", result);
return result;
} catch (Exception e) {
log.error("請(qǐng)求失敗", e);
// 失敗重試邏輯
for (int i = 0; i < 2; i++) {
try {
return restTemplate.postForObject(PAY_URL, entity, String.class);
} catch (Exception ex) { }
}
throw e;
}
}
這段代碼的問題很明顯:重復(fù)邏輯散落在各個(gè)接口調(diào)用中,維護(hù)成本極高。
如果有10個(gè)接口需要調(diào)用第三方服務(wù),就要寫10遍相同的代碼。
而攔截器鏈能把這些通用邏輯抽離成獨(dú)立組件,按順序串聯(lián)執(zhí)行,實(shí)現(xiàn)"一次定義,處處復(fù)用"。
核心調(diào)用代碼可以簡(jiǎn)化為:
return restTemplate.postForObject(PAY_URL, entity, String.class);
03 RestTemplate攔截器基礎(chǔ)
Spring提供了ClientHttpRequestInterceptor接口,所有自定義攔截器都要實(shí)現(xiàn)它。
public interface ClientHttpRequestInterceptor {
// 攔截方法:處理請(qǐng)求后通過execution繼續(xù)執(zhí)行調(diào)用鏈
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}
攔截器工作流程
我們先實(shí)現(xiàn)一個(gè)打印請(qǐng)求響應(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 // 簡(jiǎn)化日志輸出
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 記錄請(qǐng)求開始時(shí)間
long start = System.currentTimeMillis();
// 打印請(qǐng)求信息
log.info("===== 請(qǐng)求開始 =====");
log.info("URL: {}", request.getURI()); // 請(qǐng)求地址
log.info("Method: {}", request.getMethod()); // 請(qǐng)求方法(GET/POST等)
log.info("Headers: {}", request.getHeaders()); // 請(qǐng)求頭
log.info("Body: {}", new String(body, StandardCharsets.UTF_8)); // 請(qǐng)求體
// 執(zhí)行后續(xù)攔截器或?qū)嶋H請(qǐng)求
ClientHttpResponse response = execution.execute(request, body);
// 讀取響應(yīng)體(注意:默認(rèn)流只能讀一次)
String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
// 打印響應(yīng)信息
log.info("===== 請(qǐng)求結(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("耗時(shí): {}ms", System.currentTimeMillis() - start); // 計(jì)算請(qǐng)求耗時(shí)
return response;
}
}
關(guān)鍵點(diǎn):
- execution.execute(…)是調(diào)用鏈的核心,必須調(diào)用才能繼續(xù)執(zhí)行后續(xù)攔截器或發(fā)送實(shí)際請(qǐng)求。
- 響應(yīng)流response.getBody()默認(rèn)只能讀取一次,如需多次處理需緩存(后面會(huì)講解決方案)。
04 攔截器鏈實(shí)戰(zhàn):從單攔截器到多攔截器協(xié)同
SpringBoot中通過RestTemplate.setInterceptors()注冊(cè)多個(gè)攔截器,形成調(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);
// 配置請(qǐng)求工廠(支持響應(yīng)流緩存)
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 連接超時(shí)3秒
factory.setReadTimeout(3000); // 讀取超時(shí)3秒
// 用BufferingClientHttpRequestFactory包裝,解決響應(yīng)流只能讀一次的問題
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(factory));
return restTemplate;
}
}
攔截器執(zhí)行順序
按添加順序執(zhí)行(像排隊(duì)一樣),上述示例的執(zhí)行流程是:
AuthInterceptor → LoggingInterceptor → RetryInterceptor → 實(shí)際請(qǐng)求 → 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();
// 往請(qǐng)求頭添加認(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í)行請(qǐng)求,若成功直接返回響應(yīng)
return execution.execute(request, body);
} catch (HttpStatusCodeException e) {
// 處理HTTP狀態(tài)碼異常(如5xx服務(wù)器錯(cuò)誤)
if (e.getStatusCode().is5xxServerError() && retryCount < maxRetries) {
retryCount++;
log.warn("服務(wù)器錯(cuò)誤,開始第{}次重試,狀態(tài)碼:{}", retryCount, e.getStatusCode());
continue; // 繼續(xù)重試
}
// 不滿足重試條件,拋出異常
throw e;
} catch (IOException e) {
// 處理網(wǎng)絡(luò)異常(如連接超時(shí))
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ù)攔截器再讀會(huì)讀取到空數(shù)據(jù)。
原因:響應(yīng)流默認(rèn)是一次性的,讀完就關(guān)閉了。
解決方案:用BufferingClientHttpRequestFactory包裝請(qǐng)求工廠,緩存響應(yīng)流:
// 創(chuàng)建基礎(chǔ)請(qǐng)求工廠 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 包裝成支持響應(yīng)緩存的工廠 BufferingClientHttpRequestFactory bufferingFactory = new BufferingClientHttpRequestFactory(factory); // 設(shè)置到RestTemplate restTemplate.setRequestFactory(bufferingFactory); (上述代碼已在04節(jié)的配置類中體現(xiàn))
攔截器順序問題
反例:把LoggingInterceptor放在AuthInterceptor前面,日志中會(huì)看不到Authorization頭。
因?yàn)槿罩緮r截器先執(zhí)行時(shí),認(rèn)證攔截器還沒添加認(rèn)證頭。
正確順序(按職責(zé)劃分):
- 前置處理攔截器:認(rèn)證、加密、參數(shù)修改
- 日志攔截器:記錄完整請(qǐng)求(包含前置處理的結(jié)果)
- 重試/降級(jí)攔截器:處理異常情況(放在最后,確保重試時(shí)能重新執(zhí)行所有前置邏輯)
線程安全問題
RestTemplate是線程安全的,但攔截器若有成員變量,需確保線程安全!
錯(cuò)誤示例:
public class BadInterceptor implements ClientHttpRequestInterceptor {
private int count = 0; // 非線程安全的成員變量
@Override
public ClientHttpResponse intercept(...) {
count++; // 多線程環(huán)境下會(huì)出現(xiàn)計(jì)數(shù)錯(cuò)誤
}
}
解決方案:
- 攔截器避免定義可變成員變量
- 必須使用時(shí),用ThreadLocal隔離線程狀態(tài)(每個(gè)線程獨(dú)立維護(hù)變量副本)
06 測(cè)試示例
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;
// 測(cè)試接口:調(diào)用第三方服務(wù)
@GetMapping("/call-third")
public String callThirdApi() {
// 直接調(diào)用,攔截器會(huì)自動(dòng)處理認(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的攔截器鏈,我們可以將請(qǐng)求處理的通用邏輯(認(rèn)證、日志、重試等)抽離成獨(dú)立組件,實(shí)現(xiàn)代碼復(fù)用和統(tǒng)一維護(hù)。
- 攔截器鏈順序:按"前置處理→日志→重試"順序注冊(cè),確保功能正確。
- 響應(yīng)流處理:使用BufferingClientHttpRequestFactory解決流只能讀取一次問題。
- 線程安全:攔截器避免定義可變成員變量,必要時(shí)使用ThreadLocal。
- 異常處理:重試攔截器需明確重試條件(如只對(duì)5xx錯(cuò)誤重試,避免對(duì)4xx客戶端錯(cuò)誤重試)。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringBoot3使用RestTemplate請(qǐng)求接口忽略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請(qǐng)求將文件下載到本地
- SpringBoot3 RestTemplate配置與使用詳解
- SpringBoot中restTemplate請(qǐng)求存在亂碼問題的解決方法
相關(guān)文章
Spring中的ImportBeanDefinitionRegistrar接口詳解
這篇文章主要介紹了Spring中的ImportBeanDefinitionRegistrar接口詳解,ImportBeanDefinitionRegistrar接口是也是spring的擴(kuò)展點(diǎn)之一,它可以支持我們自己寫的代碼封裝成BeanDefinition對(duì)象,注冊(cè)到Spring容器中,功能類似于注解@Service @Component,需要的朋友可以參考下2023-09-09
SpringBoot使用Redis同時(shí)執(zhí)行多條命令的實(shí)現(xiàn)方法
在 Spring Boot 項(xiàng)目中高效、合理地使用 Redis 同時(shí)執(zhí)行多條命令,可以顯著提升應(yīng)用性能,下面我將為你介紹幾種主要方式、它們的典型應(yīng)用場(chǎng)景,以及如何在 Spring Boot 中實(shí)現(xiàn),需要的朋友可以參考下2025-09-09
spring boot整合netty的實(shí)現(xiàn)方法
這篇文章主要介紹了spring boot整合netty的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
java實(shí)現(xiàn)字符串四則運(yùn)算公式解析工具類的方法
今天小編就為大家分享一篇java實(shí)現(xiàn)字符串四則運(yùn)算公式解析工具類的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-07-07
Spring @Component自定義注解實(shí)現(xiàn)詳解
@Component是一個(gè)元注解,意思是可以注解其他類注解,如@Controller @Service @Repository @Aspect。官方的原話是:帶此注解的類看為組件,當(dāng)使用基于注解的配置和類路徑掃描的時(shí)候,這些類就會(huì)被實(shí)例化2022-09-09
解決子線程無法訪問父線程中通過ThreadLocal設(shè)置的變量問題
這篇文章主要介紹了解決子線程無法訪問父線程中通過ThreadLocal設(shè)置的變量問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
SpringBoot動(dòng)態(tài)更新yml文件
在系統(tǒng)運(yùn)行過程中,可能由于一些配置項(xiàng)的簡(jiǎn)單變動(dòng)需要重新打包啟停項(xiàng)目,這對(duì)于在運(yùn)行中的項(xiàng)目會(huì)造成數(shù)據(jù)丟失,客戶操作無響應(yīng)等情況發(fā)生,針對(duì)這類情況對(duì)開發(fā)框架進(jìn)行升級(jí)提供yml文件實(shí)時(shí)修改更新功能,這篇文章主要介紹了SpringBoot動(dòng)態(tài)更新yml文件2023-01-01
Java利用位運(yùn)算實(shí)現(xiàn)比較兩個(gè)數(shù)的大小
這篇文章主要為大家介紹了,在Java中如何不用任何比較判斷符(>,==,<),返回兩個(gè)數(shù)( 32 位整數(shù))中較大的數(shù),感興趣的可以了解一下2022-08-08

