SpringBoot手写RestTemplate拦截器链,掌控HTTP请求实践
作者:小马不敲代码

01 前言
在项目开发中,RestTemplate作为Spring提供的HTTP客户端工具,经常用于访问内部或三方服务。
但实际开发时,我们常常需要对请求做统一处理,比如添加认证Token、记录请求日志、设置超时时间、实现失败重试等。
要是每个接口调用都手动处理这些逻辑,不仅代码冗余,还容易出错。
02 为什么需要拦截器链?
先看一个场景:假设支付服务需要调用第三方支付接口,每次请求都得做一系列操作。
如果没有拦截器,代码会写成这样:
public String createOrder(String orderId) {
// 创建RestTemplate实例
RestTemplate restTemplate = new RestTemplate();
// 设置请求超时时间
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 连接超时3秒
factory.setReadTimeout(3000); // 读取超时3秒
restTemplate.setRequestFactory(factory);
// 设置请求头(添加认证信息)
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getToken()); // 拼接认证Token
HttpEntity<String> entity = new HttpEntity<>(headers);
// 记录请求日志
log.info("请求支付接口: {}", orderId);
try {
// 发送请求
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;
}
}
这段代码的问题很明显:重复逻辑散落在各个接口调用中,维护成本极高。
如果有10个接口需要调用第三方服务,就要写10遍相同的代码。
而拦截器链能把这些通用逻辑抽离成独立组件,按顺序串联执行,实现"一次定义,处处复用"。
核心调用代码可以简化为:
return restTemplate.postForObject(PAY_URL, entity, String.class);
03 RestTemplate拦截器基础
Spring提供了ClientHttpRequestInterceptor接口,所有自定义拦截器都要实现它。
public interface ClientHttpRequestInterceptor {
// 拦截方法:处理请求后通过execution继续执行调用链
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}
拦截器工作流程
我们先实现一个打印请求响应日志的拦截器,这是项目中最常用的功能:
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)); // 请求体
// 执行后续拦截器或实际请求
ClientHttpResponse response = execution.execute(request, body);
// 读取响应体(注意:默认流只能读一次)
String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
// 打印响应信息
log.info("===== 请求结束 =====");
log.info("Status: {}", response.getStatusCode()); // 响应状态码
log.info("Headers: {}", response.getHeaders()); // 响应头
log.info("Body: {}", responseBody); // 响应体
log.info("耗时: {}ms", System.currentTimeMillis() - start); // 计算请求耗时
return response;
}
}
关键点:
- execution.execute(…)是调用链的核心,必须调用才能继续执行后续拦截器或发送实际请求。
- 响应流response.getBody()默认只能读取一次,如需多次处理需缓存(后面会讲解决方案)。
04 拦截器链实战:从单拦截器到多拦截器协同
SpringBoot中通过RestTemplate.setInterceptors()注册多个拦截器,形成调用链。
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 // 标记为配置类,让Spring扫描加载
public class RestTemplateConfig {
@Bean // 将RestTemplate实例注入Spring容器
public RestTemplate customRestTemplate() {
// 创建拦截器列表
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new AuthInterceptor()); // 1. 认证拦截器
interceptors.add(new LoggingInterceptor()); // 2. 日志拦截器
interceptors.add(new RetryInterceptor(2)); // 3. 重试拦截器(最多重试2次)
// 创建RestTemplate并设置拦截器
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(interceptors);
// 配置请求工厂(支持响应流缓存)
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 连接超时3秒
factory.setReadTimeout(3000); // 读取超时3秒
// 用BufferingClientHttpRequestFactory包装,解决响应流只能读一次的问题
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(factory));
return restTemplate;
}
}
拦截器执行顺序
按添加顺序执行(像排队一样),上述示例的执行流程是:
AuthInterceptor → LoggingInterceptor → RetryInterceptor → 实际请求 → RetryInterceptor → LoggingInterceptor → AuthInterceptor
认证拦截器示例
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 {
// 获取认证Token(实际项目中可能从缓存或配置中心获取)
String token = getToken();
// 往请求头添加认证信息
HttpHeaders headers = request.getHeaders();
headers.set("Authorization", "Bearer " + token); // 标准Bearer认证格式
// 继续执行后续拦截器
return execution.execute(request, body);
}
// 模拟获取Token的方法
private String getToken() {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 实际为真实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; // 最大重试次数
// 构造方法指定最大重试次数
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 {
// 执行请求,若成功直接返回响应
return execution.execute(request, body);
} catch (HttpStatusCodeException e) {
// 处理HTTP状态码异常(如5xx服务器错误)
if (e.getStatusCode().is5xxServerError() && retryCount < maxRetries) {
retryCount++;
log.warn("服务器错误,开始第{}次重试,状态码:{}", retryCount, e.getStatusCode());
continue; // 继续重试
}
// 不满足重试条件,抛出异常
throw e;
} catch (IOException e) {
// 处理网络异常(如连接超时)
if (retryCount < maxRetries) {
retryCount++;
log.warn("网络异常,开始第{}次重试", retryCount, e);
continue; // 继续重试
}
// 重试次数耗尽,抛出异常
throw e;
}
}
}
}
05 实战踩坑指南
响应流读取问题
现象:日志拦截器读取响应体后,后续拦截器再读会读取到空数据。
原因:响应流默认是一次性的,读完就关闭了。
解决方案:用BufferingClientHttpRequestFactory包装请求工厂,缓存响应流:
// 创建基础请求工厂 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 包装成支持响应缓存的工厂 BufferingClientHttpRequestFactory bufferingFactory = new BufferingClientHttpRequestFactory(factory); // 设置到RestTemplate restTemplate.setRequestFactory(bufferingFactory); (上述代码已在04节的配置类中体现)
拦截器顺序问题
反例:把LoggingInterceptor放在AuthInterceptor前面,日志中会看不到Authorization头。
因为日志拦截器先执行时,认证拦截器还没添加认证头。
正确顺序(按职责划分):
- 前置处理拦截器:认证、加密、参数修改
- 日志拦截器:记录完整请求(包含前置处理的结果)
- 重试/降级拦截器:处理异常情况(放在最后,确保重试时能重新执行所有前置逻辑)
线程安全问题
RestTemplate是线程安全的,但拦截器若有成员变量,需确保线程安全!
错误示例:
public class BadInterceptor implements ClientHttpRequestInterceptor {
private int count = 0; // 非线程安全的成员变量
@Override
public ClientHttpResponse intercept(...) {
count++; // 多线程环境下会出现计数错误
}
}
解决方案:
- 拦截器避免定义可变成员变量
- 必须使用时,用ThreadLocal隔离线程状态(每个线程独立维护变量副本)
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;
// 测试接口:调用第三方服务
@GetMapping("/call-third")
public String callThirdApi() {
// 直接调用,拦截器会自动处理认证、日志、重试等逻辑
return customRestTemplate.getForObject("http://localhost:8080/mock-third-api", String.class);
}
// 模拟第三方接口
@GetMapping("/mock-third-api")
public String mockThirdApi() {
return "hello from third party";
}
}
所需依赖(只需基础的Spring Boot Starter Web):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
07 总结与扩展
通过自定义RestTemplate的拦截器链,我们可以将请求处理的通用逻辑(认证、日志、重试等)抽离成独立组件,实现代码复用和统一维护。
- 拦截器链顺序:按"前置处理→日志→重试"顺序注册,确保功能正确。
- 响应流处理:使用BufferingClientHttpRequestFactory解决流只能读取一次问题。
- 线程安全:拦截器避免定义可变成员变量,必要时使用ThreadLocal。
- 异常处理:重试拦截器需明确重试条件(如只对5xx错误重试,避免对4xx客户端错误重试)。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
