OpenFeign请求头丢失问题!OpenFeign同步调用、异步调用获取不到请求头问题!
OpenFeign请求头丢失问题!OpenFeign同步调用、异步调用获取不到请求头问题!
前言:
一般SpringBoot项目中,都会有一个鉴权的拦截器或者过滤器,例如这样:
@Bean
public HandlerInterceptor authInterceptor() {
return new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (!"zzz".equals(token)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("error");
return false;
}
return true;
}
};
}
如果请求头中没有携带正确的token,那么这个请求就会被拦截。
如果在一次HTTP请求的业务处理中,使用OpenFeign进行远程调用其它服务的接口,会发现原本HTTP请求携带的请求头token,并没有自动赋值给OpenFeign的请求头中,导致被其它服务一个鉴权拦截器所拦截,从而无法正常请求。
那么这就需要我们进行一些特定的处理,将原本HTTP请求中携带的请求头,赋值给OpenFeign的请求头中。
方法一:给Feign接口的方法增加@RequestHeader标注的参数
修改Feign接口的参数列表,增加一个参数,类型可以是Map<String, String>,也可以是org.springframework.http.HttpHeaders
,并标注@RequestHeader
。推荐类型使用HttpHeaders。OpenFeign会将标注了@RequestHeader的参数作为请求头,进行请求。
/**
* Feign接口增加HttpHeaders参数,并标注@RequestHeader
*/
@FeignClient(value = "demo2", path = "/demo")
public interface RpcService0 {
@GetMapping("/get1")
String get1(@RequestHeader HttpHeaders headers);
}
使用这种方法,需要先获取HttpHeaders,再去调用Feign接口的方法。获取HttpHeaders有两种方法:
第一种:Controller的方法里面同样增加标注@RequestHeader的HttpHeaders参数,然后调用Feign接口时传递HttpHeaders。
@GetMapping("/get1")
public String get1(@RequestHeader HttpHeaders headers) {
// 需要注意移除掉Content-Length,不然Feign远程调用会报错
headers.remove(HttpHeaders.CONTENT_LENGTH);
return rpcService.get1(headers);
}
第二种:通过RequestContextHolder获取Request对象中的对象头,并构建一个HttpHeaders。
@GetMapping("/get2")
public String get2() {
HttpHeaders httpHeaders = new HttpHeaders();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
// 需要过滤掉请求头中的Content-Length,不然可能会报错
if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
continue;
}
String header = request.getHeader(headerName);
httpHeaders.add(headerName, header);
}
return rpcService.get2(httpHeaders);
}
这种方式对代码侵入性比较大,还需要改方法的参数列表,一般不推荐使用。
方法二:实现Feign的请求拦截器RequestInterceptor,获取当前HTTP线程的请求头。
/**
* 获取当前线程的RequestAttributes对象,
* 再获取到HttpServletRequest对象,
* 将HttpServletRequest对象里的Header,
* 赋值给Feign创建的RequestTemplate中
*/
@Bean
public RequestInterceptor requestInterceptor1() {
return template -> {
// 获取当前线程的请求属性
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
// 获取当前请求的请求头
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames == null) {
return;
}
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
// 需要过滤掉请求头中的Content-Length,不然可能会报错
if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
continue;
}
String header = request.getHeader(headerName);
template.header(headerName, header);
}
};
}
优点:
简单,不需要修改业务代码。
缺点:
- 异步调用Feign接口不好处理,有些情况无法处理。这也分两种情况:
异步阻塞调用(√)
public String asyncBlockingRpc() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
try {
return rpcService.get1();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}, executorService);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
try {
return rpcService.get2();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}, executorService);
try {
return future1.get() + future2.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
这种情况,HTTP线程要等待异步线程执行完OpenFeign远程调用之后,才会返回。那么只需要在异步执行前,获取当前HTTP线程的请求属性RequestAttributes;在异步执行中,将HTTP线程的请求属性RequestAttributes,赋值给异步线程。那么异步线程去调用OpenFeign接口时,也可以通过上面定义的拦截器,获取到请求头。
异步非阻塞调用(×)
public String asyncNonBlockingRpc(String param) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 异步非阻塞操作的话,HTTP线程返回响应之后会清空RequestAttributes。这之后Feign请求拦截器中,request.getHeaderNames()会报错。
CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
rpcService.set(param);
}, executorService);
return "ok";
}
这种情况像上面一样处理就会有异常结果。HTTP线程发起异步调用后,并没有阻塞等待异步线程执行完成,就直接返回了。而异步线程中实际上是和HTTP线程共用RequestAttributes的,HTTP线程返回之后,会清空掉RequestAttributes里面的属性,那么异步线程的RequestAttributes也空了,OpenFeign拦截器自然也无法获取到请求头了。最终,这个远程调用也会被鉴权拦截器所拦截。
方法三:自定义Feign的请求拦截器RequestInterceptor,定义一个请求头持有器。
先定义一个请求头持有器。这个请求头持有器实际上就是使用一个ThreadLocal<Map<String, String>>,来存储请求头。
/**
* 请求头持有器
*/
public class RequestHeaderHolder {
private static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<>();
public static void addHeader(String headerName, String header) {
if (threadLocal.get() == null) {
threadLocal.set(new HashMap<>());
}
threadLocal.get().put(headerName, header);
}
public static Map<String, String> getHeaders() {
return threadLocal.get();
}
public static void setHeaders(Map<String, String> headers) {
threadLocal.set(headers);
}
public static void remove() {
threadLocal.remove();
}
}
那么在什么时候往RequestHeaderHolder里面存储请求头呢?这就需要定义一个拦截器了。接收到请求后,这个拦截器会获取HTTP请求头,并存储到RequestHeaderHolder。请求处理完毕后,会清空RequestHeaderHolder。
/**
* 请求头处理拦截器
* 将请求头放入RequestHeaderHolder中
*
* @return HandlerInterceptor
*/
@Bean
public HandlerInterceptor requestHeaderInterceptor() {
return new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
RequestHeaderHolder.addHeader(name, value);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHeaderHolder.remove();
}
};
}
/**
* 注册requestHeaderInterceptor
*
* @return WebMvcConfigurer
*/
@Bean
public WebMvcConfigurer requestHeaderInterceptorConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(requestHeaderInterceptor())
.addPathPatterns("/**")
.order(Ordered.HIGHEST_PRECEDENCE);
}
};
}
最后定义Feign的拦截器。这个拦截器的逻辑和第二种方法相似,只不过是从RequestHeaderHolder中获取HTTP线程的请求头,并赋值给Feign的请求头。
/**
* Feign请求拦截器
* 通过RequestHeaderHolder获取当前线程的请求头,
* 赋值给Feign创建的RequestTemplate中
*
* @return RequestInterceptor
*/
@Bean
public RequestInterceptor requestInterceptor2() {
return template -> {
Map<String, String> headers = Config3.RequestHeaderHolder.getHeaders();
if (headers != null) {
headers.forEach((headerName, header) -> {
// 需要过滤掉请求头中的Content-Length,不然可能会报错
if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
return;
}
template.header(headerName, header);
});
}
};
}
这样子就能传递HTTP线程的请求头了。
但是遇到异步Feign远程调用,同样也要另外做处理。
如果是通过创建子线程例如new Thread(() -> {}).start();
,来进行异步Feign远程调用,那么只需要RequestHeaderHolder里的ThreadLocal替换为InheritableThreadLocal,那么子线程就会继承父线程的ThreadLocalMap的值,子线程也就可以获取到父线程的请求头,进行正确的远程调用了。
但实际中大部分场景是需要使用线程池的,线程池里的线程和HTTP线程并不是父子关系,那么使用InheritableThreadLocal也没有用了。这就需要在异步执行之前获取到当前HTTP线程的请求头,在异步执行时赋值给异步线程。但这个方法比第二个方法好的就是,HTTP线程哪怕返回了,也不会清理掉已经赋值给异步线程的请求头。
例如:
@Override
public String asyncBlockingRpc() {
Map<String, String> headers = RequestHeaderHolder.getHeaders();
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
RequestHeaderHolder.setHeaders(headers);
try {
return rpcService.get1();
} finally {
RequestHeaderHolder.remove();
}
}, executorService);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
RequestHeaderHolder.setHeaders(headers);
try {
return rpcService.get2();
} finally {
RequestHeaderHolder.remove();
}
}, executorService);
try {
return future1.get() + future2.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public String asyncNonBlockingRpc(String param) {
Map<String, String> headers = RequestHeaderHolder.getHeaders();
CompletableFuture.runAsync(() -> {
RequestHeaderHolder.setHeaders(headers);
try {
Thread.sleep(1000);
rpcService.set(param);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
RequestHeaderHolder.remove();
}
}, executorService);
return "ok";
}
这种方式始终太麻烦,需要写很多无关的业务代码。我们可以借助Alibaba的TransmittableThreadLocal来对方法三进行改造,减少代码侵入性。
方法四:TransmittableThreadLocal(推荐)
首先引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.4</version>
</dependency>
然后修改RequestHeaderHolder,将ThreadLocal替换为TransmittableThreadLocal
public static class RequestHeaderHolder {
private static TransmittableThreadLocal<Map<String, String>> threadLocal = new TransmittableThreadLocal<>();
// 省略下面代码……
}
修改线程池:
假设原本的线程池是这样的
private ExecutorService executorService = Executors.newCachedThreadPool();
现在可以使用TtlExecutors.getTtlExecutorService()来包装一层
private ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newCachedThreadPool());
以后进行异步调用,就使用这个被包装过的线程池,框架会自动帮我们将HTTP线程的ThreadLocalMap的值传递给线程池中的线程,而无需像方法三那样子手动传递。
public String asyncBlockingRpc() {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> rpcService.get1(), executorService);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> rpcService.get2(), executorService);
try {
return future1.get() + future2.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
点个赞吧~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)