自定義 RestTemplate 的攔截器鏈:從原理到實(shí)踐
前言
在Spring生態(tài)中,RestTemplate是發(fā)送HTTP請求的常用工具類。而攔截器作為其核心擴(kuò)展點(diǎn),能在請求發(fā)送前、響應(yīng)返回后插入自定義邏輯,比如日志記錄、鑒權(quán)處理、參數(shù)加密等。
什么是攔截器鏈
RestTemplate的攔截器鏈?zhǔn)怯啥鄠€(gè)ClientHttpRequestInterceptor接口實(shí)現(xiàn)類組成的執(zhí)行序列。當(dāng)調(diào)用RestTemplate的getForObject、postForEntity等方法時(shí),請求會按順序經(jīng)過所有攔截器的前置處理,執(zhí)行實(shí)際請求后,響應(yīng)會按逆序經(jīng)過攔截器的后置處理。
這種鏈?zhǔn)皆O(shè)計(jì)的優(yōu)勢在于:
- 多個(gè)攔截器可獨(dú)立實(shí)現(xiàn)單一職責(zé)(如日志、鑒權(quán)、超時(shí)控制),通過組合滿足復(fù)雜需求;
- 攔截器之間解耦,可靈活添加、移除或調(diào)整順序;
- 無需修改RestTemplate核心邏輯,符合開閉原則。
工作原理
RestTemplate的請求執(zhí)行流程可簡化為三個(gè)階段:
- 前置處理:攔截器按添加順序執(zhí)行intercept方法的前置邏輯(如添加請求頭、修改參數(shù));
- 實(shí)際請求:經(jīng)過所有攔截器后,由ClientHttpRequest發(fā)送真實(shí) HTTP 請求;
- 后置處理:響應(yīng)返回后,攔截器按逆序執(zhí)行后置邏輯(如解析響應(yīng)、記錄耗時(shí))。
核心接口ClientHttpRequestInterceptor的定義如下,所有自定義攔截器都需實(shí)現(xiàn)該接口:
public interface ClientHttpRequestInterceptor {
// request:當(dāng)前請求對象;body:請求體;execution:請求執(zhí)行器(用于觸發(fā)下一個(gè)攔截器或?qū)嶋H請求)
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}其中ClientHttpRequestExecution的execute方法是關(guān)鍵——調(diào)用該方法會觸發(fā)下一個(gè)攔截器或?qū)嶋H請求,因此攔截器的邏輯通常需遵循 前置處理→調(diào)用 execute→后置處理的結(jié)構(gòu)。
實(shí)現(xiàn)
攔截器的核心價(jià)值在于專注單一功能。
日志攔截器:記錄請求響應(yīng)詳情
/**
* 日志攔截器:打印請求和響應(yīng)的關(guān)鍵信息
*/
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 前置處理:記錄請求信息
long startTime = System.currentTimeMillis();
log.info("【請求開始】URL:{},方法:{},參數(shù):{}",
request.getURI(),
request.getMethod(),
new String(body, StandardCharsets.UTF_8));
// 執(zhí)行下一個(gè)攔截器或?qū)嶋H請求
ClientHttpResponse response = execution.execute(request, body);
// 后置處理:記錄響應(yīng)信息
long endTime = System.currentTimeMillis();
log.info("【請求結(jié)束】URL:{},狀態(tài)碼:{},耗時(shí):{}ms",
request.getURI(),
response.getStatusCode(),
endTime - startTime);
return response;
}
}鑒權(quán)攔截器:自動添加認(rèn)證信息
/**
* 鑒權(quán)攔截器:為請求添加Token認(rèn)證頭
*/
public class AuthInterceptor implements ClientHttpRequestInterceptor {
private final String token;
// 通過構(gòu)造器注入Token(實(shí)際可從配置文件讀?。? public AuthInterceptor(String token) {
this.token = token;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 前置處理:添加Token請求頭
HttpHeaders headers = request.getHeaders();
headers.add("Authorization", "Bearer " + token);
// 執(zhí)行請求
ClientHttpResponse response = execution.execute(request, body);
// 后置處理:可根據(jù)響應(yīng)狀態(tài)處理Token過期(如刷新Token)
if (response.getStatusCode().is4xxClientError()
&& response.getStatusCode().value() == 401) {
log.warn("Token已過期,需執(zhí)行刷新邏輯");
// 實(shí)際場景可調(diào)用刷新Token接口并更新本地Token
}
return response;
}
}超時(shí)控制攔截器:動態(tài)調(diào)整超時(shí)時(shí)間
/**
* 超時(shí)控制攔截器:根據(jù)請求路徑設(shè)置超時(shí)
*/
public class TimeoutInterceptor implements ClientHttpRequestInterceptor {
// 普通接口超時(shí)(毫秒)
private static final int NORMAL_TIMEOUT = 3000;
// 文件上傳接口超時(shí)(毫秒)
private static final int UPLOAD_TIMEOUT = 30000;
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 前置處理:根據(jù)路徑判斷超時(shí)時(shí)間
String uri = request.getURI().getPath();
int timeout = uri.contains("/upload") ? UPLOAD_TIMEOUT : NORMAL_TIMEOUT;
// 如果是SimpleClientHttpRequestFactory,設(shè)置超時(shí)(需提前確認(rèn)請求工廠類型)
if (execution instanceof InterceptingClientHttpRequest.InterceptingRequestExecution) {
try {
// 通過反射獲取請求工廠(實(shí)際可在配置時(shí)提前綁定)
Field requestFactoryField = InterceptingClientHttpRequest.class.getDeclaredField("requestFactory");
requestFactoryField.setAccessible(true);
SimpleClientHttpRequestFactory requestFactory =
(SimpleClientHttpRequestFactory) requestFactoryField.get(execution);
requestFactory.setReadTimeout(timeout);
requestFactory.setConnectTimeout(timeout);
} catch (Exception e) {
log.error("設(shè)置超時(shí)時(shí)間失敗", e);
}
}
return execution.execute(request, body);
}
}配置攔截器鏈
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate customRestTemplate() {
// 1. 創(chuàng)建攔截器實(shí)例(可通過@Value注入配置參數(shù))
LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
AuthInterceptor authInterceptor = new AuthInterceptor("fixed-token-123456");
TimeoutInterceptor timeoutInterceptor = new TimeoutInterceptor();
// 2. 按執(zhí)行順序添加攔截器(關(guān)鍵!)
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(loggingInterceptor); // 第1個(gè):先執(zhí)行前置,最后執(zhí)行后置
interceptors.add(authInterceptor); // 第2個(gè)
interceptors.add(timeoutInterceptor); // 第3個(gè):最后執(zhí)行前置,最先執(zhí)行后置
// 3. 創(chuàng)建RestTemplate并設(shè)置攔截器
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(interceptors);
// 4. 配置請求工廠(如支持超時(shí)的SimpleClientHttpRequestFactory)
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory));
return restTemplate;
}
}攔截器順序的影響
攔截器的執(zhí)行順序遵循先進(jìn)先出(FIFO)前置,先進(jìn)后出(FILO)后置原則:
- 前置邏輯:logging → auth → timeout(按添加順序);
- 后置邏輯:timeout → auth → logging(逆序)。
舉個(gè)例子:若authInterceptor需要依賴timeoutInterceptor的超時(shí)配置,則timeoutInterceptor必須放在前面;若loggingInterceptor需要記錄最終發(fā)送的請求參數(shù)(含auth添加的Token),則authInterceptor需在其之后。
注意事項(xiàng)
- 避免耗時(shí)操作:攔截器的前置 / 后置邏輯會阻塞請求執(zhí)行,禁止在其中調(diào)用數(shù)據(jù)庫、遠(yuǎn)程接口等耗時(shí)操作;
- 不要修改響應(yīng)流:ClientHttpResponse的getBody()是輸入流,讀取后需確保后續(xù)攔截器能正常獲取(可通過BufferedReader緩存內(nèi)容);
- 異常處理:攔截器中拋出的異常會中斷請求,需通過try-catch捕獲并處理(如返回默認(rèn)響應(yīng))。
總結(jié)
RestTemplate的攔截器鏈?zhǔn)菍?shí)現(xiàn)HTTP請求擴(kuò)展的優(yōu)雅方式,通過 “單一職責(zé)” 的攔截器組合,可靈活實(shí)現(xiàn)日志、鑒權(quán)、超時(shí)控制等功能。核心要點(diǎn)包括:
- 實(shí)現(xiàn)ClientHttpRequestInterceptor接口,按前置→執(zhí)行請求→后置結(jié)構(gòu)編寫邏輯;
- 配置時(shí)明確攔截器順序,避免依賴沖突;
- 結(jié)合條件判斷和Spring特性,提升攔截器的靈活性和可維護(hù)性。
































