ClientHttpRequestInterceptor拦截器
在Spring Boot中,你可以通过拦截RestTemplate请求 并在发送前设置请求头,通常有以下两种方式实现:
1. 使用ClientHttpRequestInterceptor
通过自定义拦截器ClientHttpRequestInterceptor,在intercept方法中修改请求头,然后将拦截器添加到RestTemplate实例中。
步骤:
- 创建拦截器类实现
ClientHttpRequestInterceptor接口 - 在
intercept方法中添加或修改请求头 - 将拦截器注册到
RestTemplate
示例代码:
java
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.RestTemplate;
import java.io.IOException;
import java.util.Collections;
public class HeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 添加请求头
request.getHeaders().add("Authorization", "Bearer your-token");
request.getHeaders().add("Custom-Header", "custom-value");
return execution.execute(request, body);
}
}
// 配置RestTemplate
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new HeaderInterceptor()));
return restTemplate;
}
2. 使用RestTemplateBuilder(推荐)
通过RestTemplateBuilder可以更方便地添加拦截器,支持链式调用。
示例代码:
java
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.additionalInterceptors(new HeaderInterceptor())
.build();
}
}
3. 动态设置请求头(针对特定请求)
如果某些请求头需要动态设置(如从上下文获取token),可以在拦截器中加入逻辑:
java
public class DynamicHeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 从安全上下文或其他来源动态获取值
String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
request.getHeaders().add("Authorization", "Bearer " + token);
return execution.execute(request, body);
}
}
注意事项:
- 拦截器会应用于所有通过该
RestTemplate发起的请求 - 如果需要为不同请求设置不同请求头,可在拦截器中根据
request.getURI()判断路径 - 考虑使用
ThreadLocal存储线程安全的上下文信息(如请求ID)
如果需要更细粒度的控制,可以考虑使用RestTemplate的exchange()方法直接传入HttpEntity(包含头部信息),但这种方式需要在每次调用时显式设置。
要统一拦截所有HTTP请求(包括RestTemplate、WebClient、Feign等),可以通过以下几种方式实现:
1. 使用Spring的ClientHttpRequestInterceptor(仅适用于RestTemplate)
适用于所有通过RestTemplate发起的请求,但对其他客户端无效。
2. 使用Servlet Filter(最通用)
Filter可以拦截所有进入容器的HTTP请求,包括:
- RestTemplate发起的请求
- WebClient发起的请求
- Feign客户端发起的请求
- 任何其他HTTP客户端发起的请求
示例代码:
java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
@Order(1) // 执行顺序
public class GlobalHttpFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 添加全局请求头
if (httpRequest.getHeader("Authorization") == null) {
// 这里可以设置默认认证头或其他通用头
}
// 记录请求日志
System.out.println("Request URL: " + httpRequest.getRequestURL());
System.out.println("Method: " + httpRequest.getMethod());
chain.doFilter(request, response); // 继续执行过滤器链
}
}
3. 使用AOP面向切面编程
通过AOP拦截所有HTTP客户端调用方法:
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class HttpRequestAspect {
@Around("execution(* org.springframework.web.client.RestTemplate.*(..)) || " +
"execution(* org.springframework.web.reactive.function.client.WebClient.*(..))")
public Object aroundHttpRequest(ProceedingJoinPoint joinPoint) throws Throwable {
// 在方法执行前添加请求头
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof HttpEntity) {
HttpEntity<?> entity = (HttpEntity<?>) args[i];
HttpHeaders headers = new HttpHeaders();
headers.putAll(entity.getHeaders());
headers.add("Global-Header", "common-value");
// 替换为新的HttpEntity
args[i] = new HttpEntity<>(entity.getBody(), headers);
}
}
return joinPoint.proceed(args);
}
}
4. 使用OkHttp的Interceptor(如果使用OkHttp作为底层客户端)
java
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
public class GlobalOkHttpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
return chain.proceed(chain.request().newBuilder()
.addHeader("Authorization", "Bearer token")
.addHeader("X-Request-ID", UUID.randomUUID().toString())
.build());
}
}
5. 推荐方案:组合使用
| 拦截方式 | 适用范围 | 优点 | 缺点 |
|---|---|---|---|
| Servlet Filter | 所有HTTP请求 | 最通用,覆盖全面 | 无法区分内部/外部请求 |
| AOP | 方法级别拦截 | 精确控制,可区分业务 | 配置相对复杂 |
| ClientHttpRequestInterceptor | 仅RestTemplate | 简单易用 | 局限性大 |
| OkHttp Interceptor | 使用OkHttp的客户端 | 性能好 | 需要统一HTTP客户端 |
最佳实践建议:
- 使用Servlet Filter处理全局通用的请求头(如追踪ID、基础认证)
- 使用AOP处理业务特定的拦截逻辑
- 根据项目使用的HTTP客户端选择相应的拦截机制
根据你的需求,Servlet Filter通常是最合适的统一拦截方案。
手动拦截并设置(反射):
java
public void setRequestHeader(String key, String value) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
return;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (null == attributes) {
return;
}
HttpServletRequest request = attributes.getRequest();
Class<? extends HttpServletRequest> requestClass = request.getClass();
try {
// 反射获取request属性
Field headerField = requestClass.getDeclaredField("headerMap");
headerField.setAccessible(true);
Map<String, String> headerMap = (Map<String, String>) headerField.get(request);
headerMap.put(key, value);
} catch (Exception e) {
log.error("set header error: ", e);
}
}
ClientHttpRequestInterceptor 会在 每次 通过配置了该拦截器的 RestTemplate 实例发起 HTTP 调用之前执行。
工作原理
当你调用 RestTemplate 的任何方法(如 getForObject, postForEntity, exchange 等)时,Spring 会按以下顺序处理:
- 你调用
restTemplate.getForObject(...) - Spring 开始构建实际的 HTTP 请求
- 在执行网络调用之前 ,Spring 会遍历所有已注册的
ClientHttpRequestInterceptor - 按顺序调用每个拦截器的
intercept方法 - 在你的
intercept方法中,你可以检查和修改 请求(HttpRequest)和请求体(byte[] body) - 所有拦截器执行完毕后,最终调用
execution.execute(request, body)来真正地发送网络请求
关键特性
- 每次调用都执行 :无论这个
RestTemplate实例被调用多少次,拦截器都会在每次请求前工作。 - 适用于所有方法 :它不区分
get,post,put,delete等方法,对所有通过该RestTemplate发起的请求一视同仁。 - 可设置多个拦截器 :你可以为一个
RestTemplate添加多个拦截器,它们会按照添加的顺序依次执行。
示例场景验证
假设你有以下配置和代码:
java
// 1. 定义一个打印日志的拦截器
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
System.out.println("【拦截器执行】准备发送请求至: " + request.getURI());
// ... 可以在这里添加请求头
return execution.execute(request, body);
}
}
// 2. 将拦截器添加到 RestTemplate
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor()));
return restTemplate;
}
// 3. 在服务中多次使用 RestTemplate
@Service
public class MyService {
@Autowired
private RestTemplate restTemplate;
public void fetchData() {
// 第一次调用
String result1 = restTemplate.getForObject("https://api.example.com/data/1", String.class);
// 第二次调用
String result2 = restTemplate.getForObject("https://api.example.com/data/2", String.class);
}
}
当你调用 myService.fetchData() 时,控制台会输出:
【拦截器执行】准备发送请求至: https://api.example.com/data/1
【拦截器执行】准备发送请求至: https://api.example.com/data/2
这清楚地证明了拦截器为每次请求都执行了一次。
总结
你可以放心地依赖 ClientHttpRequestInterceptor 来执行诸如添加认证令牌、设置公共请求头、记录请求日志、重试逻辑等需要在每个请求发出前 完成的统一操作。它的设计初衷就是为此类场景服务的。
Attribute和Header
- Attribute(属性) 是服务端内部 的"便签",用于在一次请求的生命周期内 ,在服务器内部的各个组件(如Controller、Service、Interceptor、Filter、View等)之间传递数据。客户端(如浏览器)完全不知道它的存在。
- Header(头信息) 是客户端和服务器之间 正式通信协议(HTTP)的一部分,用于传递元数据(如内容类型、认证信息、缓存控制等)。它对客户端是可见的,并且通常会影响请求/响应的处理方式。
下面是一个详细的对比表格,帮助你更清晰地理解:
| 特性 | Attribute(属性) | Header(头信息) |
|---|---|---|
| 作用域 | 服务器端的一次请求生命周期内(Request Scope)** | HTTP协议本身 ,在客户端和服务器之间传输 |
| 可见性 | 仅服务器内部可见(如Filter, Interceptor, Controller, Service, JSP/Thymeleaf等) | 客户端和服务器都可见,可通过浏览器开发者工具或抓包工具查看 |
| 用途 | 在服务器处理请求的过程中,在不同组件间传递数据 。例如: - Filter校验用户后,将用户信息存入attribute供Controller使用 - Interceptor计算耗时后,将耗时结果存入attribute | 传递HTTP协议的元数据 。例如: - Content-Type : 声明请求体/响应体的格式(如application/json) - Authorization : 传递认证凭证(如Bearer token) - User-Agent : 声明客户端类型 - Custom-Header : 自定义业务头(如X-Request-ID用于全链路追踪) |
| 生命周期 | 从请求进入服务器开始,到服务器返回响应结束。请求结束即销毁。 | 请求头(Request Header) 随请求从客户端发往服务器。 响应头(Response Header) 随响应从服务器发往客户端。 |
| 数据类型 | 可以是任何Java对象(Object) |
只能是字符串 (String)或字符串数组(String[]) |
| 操作方式 (Java) | request.setAttribute("key", value); Object value = request.getAttribute("key"); |
获取请求头: String value = request.getHeader("Header-Name"); 设置响应头: response.setHeader("Header-Name", "value"); response.addHeader("Header-Name", "value"); |
类比帮助理解
你可以把它们想象成寄送一个实体包裹:
- Header 就像是包裹外面的快递单 。上面写明了收件人地址、寄件人信息、包裹类型(易碎品)、是否需要代收款等。快递员和收发货双方都能看到这个单子,并根据上面的信息来处理包裹。
- Attribute 就像是包裹里面的一张内部纸条 ,是发货人写给仓库分拣员的,比如"这是VIP客户的加急订单,优先处理"。只有打开包裹的内部人员(服务器)才能看到这张纸条,收货人(客户端)根本不知道它的存在。
代码示例
1. 操作 Header (在Servlet Filter或Controller中)
java
// 从客户端发来的请求中获取头信息
String authHeader = httpRequest.getHeader("Authorization");
String userAgent = httpRequest.getHeader("User-Agent");
// 向客户端发送响应时设置头信息
httpResponse.setHeader("Content-Type", "application/json");
httpResponse.addHeader("X-Custom-Header", "MyValue");
2. 操作 Attribute (在Filter, Interceptor, Controller中)
java
// 在Filter中设置属性
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 认证成功后,将用户信息存入attribute
User user = authenticateUser(httpRequest);
httpRequest.setAttribute("currentUser", user); // 存入任何对象
chain.doFilter(request, response);
}
// 在Controller中获取和使用属性
@GetMapping("/api/profile")
public ResponseEntity getUserProfile(HttpServletRequest request) {
// 从attribute中取出之前存入的对象
User currentUser = (User) request.getAttribute("currentUser");
// ... 使用user对象处理业务
return ResponseEntity.ok(currentUser.getProfile());
}
总结
记住这个最简单的原则:
- 需要让客户端知晓或受其影响 的信息,用 Header(如认证、缓存、内容协商)。
- 只在服务器内部处理过程中 需要临时携带的数据,用 Attribute(如用户身份、计算中间结果、页面渲染数据)。