SpringBoot自定義RestTemplate的攔截器鏈的實(shí)戰(zhàn)指南
引言
在項(xiàng)目開發(fā)中,RestTemplate作為Spring提供的HTTP客戶端工具,經(jīng)常用于訪問(wèn)內(nèi)部或三方服務(wù)。
但在實(shí)際項(xiàng)目中,我們往往需要對(duì)請(qǐng)求進(jìn)行統(tǒng)一處理:比如添加認(rèn)證Token、記錄請(qǐng)求日志、設(shè)置超時(shí)時(shí)間、實(shí)現(xiàn)失敗重試等。
如果每個(gè)接口調(diào)用都手動(dòng)處理這些邏輯,不僅代碼冗余,還容易出錯(cuò)。
一、為什么需要攔截器鏈?
先看一個(gè)場(chǎng)景:假設(shè)我們的支付服務(wù)需要調(diào)用第三方支付接口,每次請(qǐng)求都要做這些事
- 添加
Authorization
請(qǐng)求頭(認(rèn)證Token) - 記錄請(qǐng)求URL、參數(shù)、耗時(shí)(日志審計(jì))
- 設(shè)置3秒超時(shí)時(shí)間(防止阻塞)
- 失敗時(shí)重試2次(網(wǎng)絡(luò)抖動(dòng)處理)
如果沒(méi)有攔截器,代碼會(huì)寫成這樣:
// 調(diào)用第三方支付接口 public String createOrder(String orderId) { // 1. 創(chuàng)建RestTemplate(重復(fù)10次) RestTemplate restTemplate = new RestTemplate(); // 2. 設(shè)置超時(shí)(重復(fù)10次) SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(3000); restTemplate.setRequestFactory(factory); // 3. 添加認(rèn)證頭(重復(fù)10次) HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + getToken()); HttpEntity<String> entity = new HttpEntity<>(headers); // 4. 記錄日志(重復(fù)10次) log.info("請(qǐng)求支付接口: {}", orderId); try { String result = restTemplate.postForObject(PAY_URL, entity, String.class); log.info("請(qǐng)求成功: {}", result); return result; } catch (Exception e) { log.error("請(qǐng)求失敗", e); // 5. 重試(重復(fù)10次) for (int i = 0; i < 2; i++) { try { return restTemplate.postForObject(PAY_URL, entity, String.class); } catch (Exception ex) { /* 重試邏輯 */ } } throw e; } }
這段代碼的問(wèn)題很明顯:重復(fù)邏輯散落在各個(gè)接口調(diào)用中,維護(hù)成本極高。如果有10個(gè)接口需要調(diào)用第三方服務(wù),就要寫10遍相同的代碼。
而攔截器鏈能把這些通用邏輯抽離成獨(dú)立組件,按順序串聯(lián)執(zhí)行,實(shí)現(xiàn)“一次定義,處處復(fù)用”。
// 重構(gòu)后:一行搞定所有通用邏輯 return restTemplate.postForObject(PAY_URL, entity, String.class);
二、RestTemplate攔截器基礎(chǔ)
2.1 核心接口:ClientHttpRequestInterceptor
Spring提供了ClientHttpRequestInterceptor
接口,所有自定義攔截器都要實(shí)現(xiàn)它
public interface ClientHttpRequestInterceptor { // request:請(qǐng)求對(duì)象(可修改請(qǐng)求頭、參數(shù)) // body:請(qǐng)求體字節(jié)數(shù)組 // execution:執(zhí)行器(用于調(diào)用下一個(gè)攔截器) ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException; }
攔截器工作流程:
- 攔截器可以修改請(qǐng)求(如添加頭信息、修改參數(shù))
- 通過(guò)
execution.execute(request, body)
調(diào)用下一個(gè)攔截器 - 最終執(zhí)行器會(huì)發(fā)送實(shí)際HTTP請(qǐng)求,返回響應(yīng)
- 攔截器可以處理響應(yīng)(如記錄日志、解析響應(yīng)體)
2.2 第一個(gè)攔截器:日志攔截器
先實(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 public class LoggingInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 1. 記錄請(qǐng)求信息 long start = System.currentTimeMillis(); log.info("===== 請(qǐng)求開始 ====="); log.info("URL: {}", request.getURI()); log.info("Method: {}", request.getMethod()); log.info("Headers: {}", request.getHeaders()); log.info("Body: {}", new String(body, StandardCharsets.UTF_8)); // 2. 執(zhí)行下一個(gè)攔截器(關(guān)鍵!不調(diào)用會(huì)中斷鏈條) ClientHttpResponse response = execution.execute(request, body); // 3. 記錄響應(yīng)信息 String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); log.info("===== 請(qǐng)求結(jié)束 ====="); log.info("Status: {}", response.getStatusCode()); log.info("Headers: {}", response.getHeaders()); log.info("Body: {}", responseBody); log.info("耗時(shí): {}ms", System.currentTimeMillis() - start); return response; } }
關(guān)鍵點(diǎn):
execution.execute(...)
是調(diào)用鏈的核心,必須調(diào)用才能繼續(xù)執(zhí)行- 響應(yīng)流
response.getBody()
只能讀取一次,如需多次處理需緩存(后面會(huì)講解決方案)
三、攔截器鏈實(shí)戰(zhàn):從單攔截器到多攔截器協(xié)同
3.1 注冊(cè)攔截器鏈
SpringBoot中通過(guò)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 public class RestTemplateConfig { @Bean public RestTemplate customRestTemplate() { // 1. 創(chuàng)建攔截器列表(順序很重要?。? List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(); interceptors.add(new AuthInterceptor()); // 1. 認(rèn)證攔截器(先加請(qǐng)求頭) interceptors.add(new LoggingInterceptor()); // 2. 日志攔截器(記錄完整請(qǐng)求) interceptors.add(new RetryInterceptor(2)); // 3. 重試攔截器(最后處理失敗) // 2. 創(chuàng)建RestTemplate并設(shè)置攔截器 RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(interceptors); // 3. 解決響應(yīng)流只能讀取一次的問(wèn)題(關(guān)鍵配置?。? SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); // 連接超時(shí)3秒 factory.setReadTimeout(3000); // 讀取超時(shí)3秒 restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(factory)); return restTemplate; } }
攔截器執(zhí)行順序:按添加順序執(zhí)行(像排隊(duì)一樣),上述示例的執(zhí)行流程是:
AuthInterceptor → LoggingInterceptor → RetryInterceptor → 實(shí)際請(qǐng)求 → RetryInterceptor → LoggingInterceptor → AuthInterceptor
3.2 核心攔截器實(shí)現(xiàn)
(1)認(rèn)證攔截器:自動(dòng)添加Token
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 { // 1. 從緩存獲取Token(實(shí)際項(xiàng)目可能從Redis或配置中心獲?。? String token = getToken(); // 2. 添加Authorization請(qǐng)求頭 HttpHeaders headers = request.getHeaders(); headers.set("Authorization", "Bearer " + token); // Bearer認(rèn)證格式 // 3. 傳遞給下一個(gè)攔截器 return execution.execute(request, body); } // 模擬從緩存獲取Token private String getToken() { return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 實(shí)際項(xiàng)目是真實(shí)Token } }
(2)重試攔截器:失敗自動(dòng)重試
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ù) public RetryInterceptor(int maxRetries) { this.maxRetries = maxRetries; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { int retryCount = 0; while (true) { try { // 執(zhí)行請(qǐng)求(調(diào)用下一個(gè)攔截器或?qū)嶋H請(qǐng)求) return execution.execute(request, body); } catch (HttpStatusCodeException e) { // 只對(duì)5xx服務(wù)器錯(cuò)誤重試(業(yè)務(wù)錯(cuò)誤不重試) if (e.getStatusCode().is5xxServerError() && retryCount < maxRetries) { retryCount++; log.warn("服務(wù)器錯(cuò)誤,開始第{}次重試,狀態(tài)碼:{}", retryCount, e.getStatusCode()); continue; } throw e; // 非5xx錯(cuò)誤或達(dá)到最大重試次數(shù) } catch (IOException e) { // 網(wǎng)絡(luò)異常重試(如連接超時(shí)、DNS解析失?。? if (retryCount < maxRetries) { retryCount++; log.warn("網(wǎng)絡(luò)異常,開始第{}次重試", retryCount, e); continue; } throw e; } } } }
四、實(shí)戰(zhàn)踩坑指南
4.1 響應(yīng)流只能讀取一次問(wèn)題
現(xiàn)象:日志攔截器讀取響應(yīng)體后,后續(xù)攔截器再讀會(huì)讀取到空數(shù)據(jù)。
原因:響應(yīng)流默認(rèn)是一次性的,讀完就關(guān)閉了。
解決方案:用BufferingClientHttpRequestFactory
包裝請(qǐng)求工廠,緩存響應(yīng)流:
// 創(chuàng)建支持響應(yīng)緩存的工廠(已在3.1節(jié)配置中體現(xiàn)) SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); BufferingClientHttpRequestFactory bufferingFactory = new BufferingClientHttpRequestFactory(factory); restTemplate.setRequestFactory(bufferingFactory);
4.2 攔截器順序不當(dāng)導(dǎo)致功能異常
反例:把LoggingInterceptor
放在AuthInterceptor
前面,日志中會(huì)看不到Authorization
頭,因?yàn)槿罩緮r截器先執(zhí)行時(shí)還沒(méi)添加認(rèn)證頭。
正確順序(按職責(zé)劃分):
- 1. 前置處理攔截器:認(rèn)證、加密、參數(shù)修改
- 2. 日志攔截器:記錄完整請(qǐng)求
- 3. 重試/降級(jí)攔截器:處理異常情況
4.3 線程安全問(wèn)題
RestTemplate
是線程安全的,但攔截器若有成員變量,需確保線程安全!
錯(cuò)誤示例
public class BadInterceptor implements ClientHttpRequestInterceptor { private int count = 0; // ? 多線程共享變量,會(huì)導(dǎo)致計(jì)數(shù)錯(cuò)誤 @Override public ClientHttpResponse intercept(...) { count++; // 危險(xiǎn)!多線程下count值不準(zhǔn) // ... } }
解決方案
- 攔截器避免定義可變成員變量
- 必須使用時(shí),用
ThreadLocal
隔離線程狀態(tài)
五、測(cè)試示例
5.1 測(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 { @Autowired private RestTemplate customRestTemplate; // 注入自定義的RestTemplate @GetMapping("/call-api") public String callThirdApi() { // 調(diào)用第三方API,攔截器自動(dòng)處理認(rèn)證、日志、重試 return customRestTemplate.getForObject("http://localhost:8080/mock-third-api", String.class); } // 模擬一個(gè)三方接口 @GetMapping("/mock-third-api") public String mockThirdApi() { return "hello"; } }
5.2 依賴與配置
只需基礎(chǔ)的Spring Boot Starter Web:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
六、總結(jié)與擴(kuò)展
通過(guò)自定義RestTemplate的攔截器鏈,我們可以將請(qǐng)求處理的通用邏輯(認(rèn)證、日志、重試等)抽離成獨(dú)立組件,實(shí)現(xiàn)代碼復(fù)用和統(tǒng)一維護(hù)。
核心要點(diǎn)
- 1. 攔截器鏈順序:按“前置處理→日志→重試”順序注冊(cè),確保功能正確
- 2. 響應(yīng)流處理:使用
BufferingClientHttpRequestFactory
解決流只能讀取一次問(wèn)題 - 3. 線程安全:攔截器避免定義可變成員變量,必要時(shí)使用
ThreadLocal
- 4. 異常處理:重試攔截器需明確重試條件(如只對(duì)5xx錯(cuò)誤重試)
以上就是SpringBoot自定義RestTemplate的攔截器鏈的實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot RestTemplate攔截器鏈的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot對(duì)接小程序微信支付的實(shí)現(xiàn)
本文主要介紹了SpringBoot對(duì)接小程序微信支付的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧<BR>2023-09-09實(shí)現(xiàn)Java刪除一個(gè)集合的多個(gè)元素
Java中的For each實(shí)際上使用的是iterator進(jìn)行處理的。而iterator是不允許集合在iterator使用期間刪除的。而我在for each時(shí),從集合中刪除了一個(gè)元素,這導(dǎo)致了iterator拋出了ConcurrentModificationException,下面來(lái)看看到底怎么回事。2016-08-08解決mac最新版intellij idea崩潰閃退crash的問(wèn)題
這篇文章主要介紹了解決mac最新版intellij idea崩潰閃退crash的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Spring Boot攔截器Interceptor與過(guò)濾器Filter深度解析(區(qū)別、實(shí)現(xiàn)與實(shí)戰(zhàn)指南)
這篇文章主要介紹了Spring Boot攔截器Interceptor與過(guò)濾器Filter深度解析(區(qū)別、實(shí)現(xiàn)與實(shí)戰(zhàn)指南),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2025-05-05自定義Jackson的ObjectMapper如何實(shí)現(xiàn)@ResponseBody的自定義渲染
這篇文章主要介紹了自定義Jackson的ObjectMapper如何實(shí)現(xiàn)@ResponseBody的自定義渲染,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07FeignClient中name和url屬性的作用說(shuō)明
這篇文章主要介紹了FeignClient中name和url屬性的作用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06