SpringBoot自定义RestTemplate的拦截器链的实战指南
目录
- 引言
- 一、为什么需要拦截器链?
- 二、RestTemplate拦截器基础
- 2.1 核心接口:ClientHttpRequestInterceptor
- 2.2 第一个拦截器:日志拦截器
- 三、拦截器链实战:从单拦截器到多拦截器协同
- 3.1 注册拦截器链
- 3.2 核心拦截器实现
- (1)认证拦截器:自动添加Token
- (2)重试拦截器:失败自动重试
- 四、实战踩坑指南
- 4.1 响应流只能读取一次问题
- 4.2 拦截器顺序不当导致功能异常
- 4.3 线程安全问题
- 五、测试示例
- 5.1 测试接口
- 5.2 依赖与配置
- 六、总结与扩展
- 核心要点
引言
在项目开发中,RestTemplate作为Spring提供的HTTP客户端工具,经常用于访问内部或三方服务。
但在实际项目中,我们往往需要对请求进行统一处理:比如添加认证Token、记录请求日志、设置超时时间、实现失败重试等。
如果每个接口调用都手动处理这些逻辑,不仅代码冗余,还容易出错。
一、为什么需要拦截器链?
先看一个场景:假设我们的支付服务需要调用第三方支付接口,每次请求都要做这些事
- 添加
Authorization
请求头(认证Token) - 记录请求URL、参数、耗时(日志审计)
- 设置3秒超时时间(防止阻塞)
- 失败时重试2次(网络抖动处理)
如果没有拦截器,代码会写成这样:
// 调用第三方支付接口 public String createOrder(String orderId) { // 1. 创建RestTemplate(重复10次) RestTemplate restTemplate = new RestTemplate(); // 2. 设置超时(重复10次) SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(3000); restTemplate.setRequestFactory(factory); // 3. 添加认证头(重复10次) HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + getToken()); HttpEntity<String> entity = new HttpEntity<>(headers); // 4. 记录日志(重复10次) log.info("请求支付接口: {}", orderId); try { String result = restTemplate.postForObject(PAY_URL, entity, String.class); log.info("请求成功: {}", result); return result; } catch (Exception e) { log.error("请求失败", e); // 5. 重试(重复10次) 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);
二、RestTemplate拦截器基础
2.1 核心接口:ClientHttpRequestInterceptor
Spring提供了ClientHttpRequestInterhttp://www.devze.comceptor
接口,所有自定义拦截器都要实现它
public interface ClientHttpRequestInterceptor { // request:请求对象(可修改请求头、参数) // body:请求体字节数组 // execution:执行器(用于调用下一个拦截器) ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException; }
拦截器工作流程:
- 拦截器可以修改请求(如添加头信息、修改参数)
- 通过
execution.execute(request, body)
调用下一个拦截器 - 最终执行器会发送实际HTTP请求,返回响应
- 拦截器可以处理响应(如记录日志、解析响应体)
2.2 第一个拦截器:日志拦截器
先实现一个打印请求响应日志的拦截器,这是项目中最常用的功能:
import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.LHdwgqrlQAspringframework.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. 记录请求信息 long start = System.currentTimeMillis(); log.info("===== 请求开始 ====="); 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. 执行下一个拦截器(关键!不调用会中断链条) ClientHttpResponse response = execution.execute(request, body); // 3. 记录响应信息 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()
只能读取一次,如需多次处理需缓存(后面会讲解决方案)
三、拦截器链实战:从单拦截器到多拦截器协同
3.1 注册拦截器链
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 public class RestTemplateConfig { @Bean public RestTemplate customRestTemplate() { // 1. 创建拦截器列表(顺序很重要!) 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); // 3. 解决响应流只能读取一次的问题(关键配置!) SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); // 连接超时3秒 factory.setReadTimeout(3000); // 读取超时3秒 restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(factory)); return restTemplate; } }
拦截器执行顺序:按添加顺序执行(像排队一样),上述示例的执行流程是:
AuthInterceptor → LoggingInterceptor → RetryInterceptor → 实际请求 → RetryInterceptor → LoggingInterceptor → AuthInterceptor
3.2 核心拦截器实现
(1)认证拦截器:自动添加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(实际项目可能从Redis或配置中心获取) http://www.devze.comString token = getToken(); // 2. 添加Authorization请求头 HttpHeaders headers = request.getHeaders(); headers.set("Authorization", "Bearer " + token); // Bearer认证格式 // 3. 传递给下一个拦截器 return execution.execute(request, body); } // 模拟从缓存获取Token private String getToken() { return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 实际项目是真实Token } }
(2)重试拦截器:失败自动重试
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) { // 只对5xx服务器错误重试(业务错误不重试) if (e.getStatusCode().is5xxServerError() && retryCount < maxRetries) { retryCount++; log.warn("服务器错误,开始第{}次重试,状态码:{}", retryCount, e.getStatusCode()); continue; } throw e; // 非5xx错误或达到最大重试次数 } catch (IOException e) { // 网络异常重试(如连接超时、DNS解析失败) if (retryCount < maxRetries) { retryCount++; log.warn("网络异常,开始第{}次重试", retryCount, e); continue; } throw e; } } } }
四、实战踩坑指南
4.1 响应流只能读取一次问题
现象:日志拦截器读取响应体后,后续拦截器再读会读取到空数据。
原因:响应流默认是一次性的,读完就关闭了。解决方案:用BufferingClientHttpRequestFactory
包装请求工厂,缓存响应流:
// 创建支持响应缓存的工厂(已在3.1节配置中体现) SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); BufferingClientHttpRequestFactory bufferingFactory = new BufferingClientHttpRequestFactory(factory); restTemplate.setRequestFactory(bufferingFactory);
4.2 拦截器顺序不当导致功能异常
反例:把LoggingInterceptor
放在AuthInterceptor
前面,日志中会看不到Authorization
头,因为日志拦截器先执行时还没添加认证头。
正确顺序(按职责划分):
- 1. 前置处理拦截器:认证、加密、参数修改
- 2. 日志拦截器:记录完整请求
- 3. 重试/降级拦截器:处理异常情况
4.3 线程安全问题
RestTemplate
是线程安全的,但拦截器若有成员变量,需确保线程安全!
错误示例
public class BadInterceptor javascriptimplements ClientHttpRequestInterceptor { private int count = 0; // ❌ 多线程共享变量,会导致计数错误 @Override public ClientHttpResponse intercept(...) { count++; // 危险!多线程下count值不准 // ... } }
解决方案
- 拦截器避免定义可变成员变量
- 必须使用时,用
ThreadLocal
隔离线程状态
五、测试示例
5.1 测试接口
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() { // 调用第三方API,拦截器自动处理认证、日志、重试 return customRestTemplate.getForObject("http://localhost:8080/mock-third-api", String.class); } // 模拟一个三方接口 @GetMapping("/mock-third-api") public String mockThirdApi() { return "hello"; } }
5.2 依赖与配置
只需基础的Spring Boot Starter Web:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifhttp://www.devze.comactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
六、总结与扩展
通过自定义RestTemplate的拦截器链,我们可以将请求处理的通用逻辑(认证、日志、重试等)抽离成独立组件,实现代码复用和统一维护。
核心要点
- 1. 拦截器链顺序:按“前置处理→日志→重试”顺序注册,确保功能正确
- 2. 响应流处理:使用
BufferingClientHttpRequestFactory
解决流只能读取一次问题 - 3. 线程安全:拦截器避免定义可变成员变量,必要时使用
ThreadLocal
- 4. 异常处理:重试拦截器需明确重试条件(如只对5xx错误重试)
以上就是SpringBoot自定义RestTemplate的拦截器链的实战指南的详细内容,更多关于SpringBoot RestTemplate拦截器链的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论