异步线程拿请求头

异步线程获取request,获取请求头数据和参数被清空问题处理

dawnStart

已于 2023-11-21 17:54:12 修改

阅读量308
收藏 2

点赞数
文章标签: tomcat spring boot java
版权
其实在tomcat中,request和response是会被复用的,tomcat会维护一个请求池,每次都会从中拿到request设置参数,然后开始一次请求,然后请求结束响应后,会将request和response重置,然后将其放到请求池中,等待后续的复用.

有时我们在异步线程外面使用RequestContextHolder.currentRequestAttributes();
获取到了当前的请求对象,然后再放到异步线程中
RequestContextHolder.setRequestAttributes(requestAttributes);
然后在异步线程中获取请求HttpServletRequest,然后拿请求头,可能获取的是空的,这是因为在异步线程执行完之前,外面的请求线程已经结束了,然后request就被tomcat重置放入池中了,所以导致request请求头,请求参数被重置了,解决这个问题有两种方案
方案一: 将当前请求标记为异步请求,这样tomcat在回收request的时候,就会等待,等待异步完成

AsyncContext asyncContext = request.startAsync();
// todo 这里执行你想要操作的事情,完成后记得调用complete
asyncContext.complete();

// 上面的方案有缺陷,tomcat会一直等待request异步执行完成调用complete方法,
//导致前端请求会一直转圈圈,所以可以用下面的方案,让tomcat再起一个线程执行你的异步方法
// 这里也不太好,用的是tomcat的线程池,如果想用自己定义的线程池,还是不太行
// 所以可以用下面的方案二
AsyncContext asyncContext = request.startAsync();
asyncContext.start(()->{

     // todo 做你要做的事
     asyncContext.complete();
 });

方案二:
拷贝原线程的request,然后再放入异步线程
工具类的代码如下

import org.apache.catalina.connector.Request;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class RequestCopy {

private RequestCopy() {
    throw new IllegalStateException("Utility class");
}

/**
 * 拷贝当前线程中的request
 * 注意:仅拷贝了需要的header,异步调用feign接口时所需要的
 * @return
 */
public static RequestAttributesCopy copyCurrentThreadRequestAttributes(){
    ServletRequestAttributes requestAttributes = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes());
    HttpServletRequest request = requestAttributes.getRequest();
    return copyToRequestAttributes(request);
}

/**
 * 拷贝request中的header
 * 注意:仅拷贝了需要的header,异步调用feign接口时所需要的
 * @param request
 * @return
 */
public static RequestAttributesCopy copyToRequestAttributes(HttpServletRequest request) {
    return new RequestAttributesCopy(copyToRequest(request));
}

/**
 * 拷贝request
 * 注意:仅拷贝了需要的header,异步调用feign接口时所需要的
 * @param request
 * @return
 */
public static HttpServletRequestCopy copyToRequest(HttpServletRequest request) {
    Map<String, String> headerMap = new HashMap();
    Enumeration<String> headers = request.getHeaderNames();
    if (headers != null) {
        while (headers.hasMoreElements()) {
            String header = (String) headers.nextElement();
                headerMap.put(header, request.getHeader(header));
            }
        }
    }
    return new HttpServletRequestCopy(headerMap);
}

public static class RequestAttributesCopy extends ServletRequestAttributes {

    public RequestAttributesCopy(HttpServletRequestCopy request) {
        super(request);
    }
}

public static class HttpServletRequestCopy extends Request {

// 自己定义的请求头保存,看自己的需求来,如果还想获取参数,那就加个参数保存就好
private final Map<String, String> header;

    public HttpServletRequestCopy(Map<String, String> header) {
        super(null);
        if (header == null) {
            header = Collections.emptyMap();
        }
        this.header = header;
    }

// 这里重写你需要获取的参数,我这边就只用获取请求头,所以就重写了请求头的获取方法
@Override
public String getHeader(String name) {
return header.get(name);
}

    @Override
    public Enumeration<String> getHeaderNames() {
        return Collections.enumeration(header.keySet());
    }
}

}

使用如下

// 异步导出,这里是传递request到异步线程中,传递token需要(需要拷贝一份,不然请求结束了,原来的request会被清空)
RequestCopy.RequestAttributesCopy requestAttributesCopy = RequestCopy.copyCurrentThreadRequestAttributes();
new Thread(()->{
RequestContextHolder.setRequestAttributes(requestAttributesCopy);
// todo 做你想做的事
}).start();

<think>嗯,用户之前问过如何在异步代码中调用Feign解决潜在问题,现在他们又具体问到多线程异步Feign调用请求头丢失的问题。我需要先回顾一下之前的回答,确保这次的内容不重复,但又能深入解决这个特定问题。 用户可能在使用Feign进行异步调用,发现原本在主线程中的请求头(比如认证信息、跟踪ID等)在子线程中丢失了。这是因为异步执行,子线程无法自动继承主线程的上下文,导致Feign客户端在发送请求缺少必要的头信息。 首先,我需要确定请求头丢失的具体原因。在Spring中,请求上下文通常是通过ThreadLocal存储的,比如RequestContextHolder。当切换线程,子线程无法访问父线程的ThreadLocal变量,所以需要手动传递这些上下文信息。 接下来,解决方案有几个方向。之前提到过手动传递上下文,比如复制RequestAttributes到子线程中。这可能是一个基础方法,但用户可能需要更详细的步骤或者更多的选项。此外,还可以考虑使用Spring Cloud Sleuth来自动处理跟踪ID的传递,或者使用Hystrix的上下文。另外,如果项目使用了响应式编程,WebClient可能也是一个解决方案,但用户的问题明确提到了Feign,所以可能需要更直接的方法。 我需要确保回答覆盖不同的方法,并给出具体的代码示例,比如使用装饰线程池、RequestContextHolder的设置、或者使用MDC来传递日志相关的信息。同,可能还要提到配置Feign的拦截器,在异步调用手动添加请求头,这样即使上下文丢失,也能通过拦截器重新注入头信息。 另外,用户可能对某些工具或库不熟悉,比如Sleuth或Hystrix,需要简要说明它们的作用,并给出配置示例。还要注意版本兼容性,比如Spring Boot或Cloud的版本不同,配置方式可能有差异,需要提醒用户根据实际情况调整。 还需要考虑异常情况的处理,比如在异步任务中如何确保上下文正确清理,避免内存泄漏或上下文污染。例如,在使用完RequestAttributes后,是否需要手动清除,或者使用try-finally块来保证。 最后,总结最佳实践,帮助用户根据具体情况选择最合适的解决方案,比如简单场景手动复制,复杂系统使用Sleuth或响应式客户端等。同,可能还需要提到测试方法,比如如何验证请求头是否正确传递,确保解决方案的有效性。</think>在异步或多线程环境下使用 Feign ,由于线程切换会导致 **ThreadLocal 上下文丢失**(如请求头、安全上下文、TraceID 等),以下是针对性解决方案: --- ### 问题核心原因 - **Spring 的请求上下文(`RequestContextHolder`)基于 `ThreadLocal`**,异步线程无法继承主线程的上下文。 - **Feign 的请求拦截器默认从当前线程获取上下文**,导致异步线程请求头为空。 --- ### 解决方案 #### 方案 1:手动传递请求头(简单场景) 在异步任务中 **主动复制主线程请求头**,并设置到 Feign 调用线程的上下文中: ```java @Async public CompletableFuture<User> asyncCall() { // 1. 从主线程捕获请求上下文 RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); return CompletableFuture.supplyAsync(() -> { try { // 2. 绑定到当前异步线程 RequestContextHolder.setRequestAttributes(attributes); // 3. 调用 Feign(此能读取原请求头) return userFeignClient.getUserById(1); } finally { // 4. 清理线程上下文(避免内存泄漏) RequestContextHolder.resetRequestAttributes(); } }, customTaskExecutor); // 指定独立线程池 } ``` --- #### 方案 2:装饰线程池(通用场景) **通过线程池装饰器自动传递上下文**,避免每次手动复制: ```java // 自定义线程池装饰器 public class ContextAwareExecutor implements Executor { private final Executor delegate; public ContextAwareExecutor(Executor delegate) { this.delegate = delegate; } @Override public void execute(Runnable command) { RequestAttributes context = RequestContextHolder.getRequestAttributes(); delegate.execute(() -> { try { RequestContextHolder.setRequestAttributes(context); command.run(); } finally { RequestContextHolder.resetRequestAttributes(); } }); } } // 配置线程池 @Bean("contextAwareExecutor") public Executor contextAwareExecutor() { return new ContextAwareExecutor(Executors.newFixedThreadPool(10)); } // 使用示例 @Async("contextAwareExecutor") public CompletableFuture<User> asyncCall() { return CompletableFuture.supplyAsync(() -> userFeignClient.getUserById(1)); } ``` --- #### 方案 3:Feign 拦截器 + 显式传参(精准控制) 通过 **自定义 Feign 拦截器** 强制注入关键请求头: ```java // 1. 定义拦截器 public class HeaderForwardInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // 从当前线程获取请求头(需提前设置) String token = RequestContextHolder.getRequestAttributes() .getAttribute("Authorization", RequestAttributes.SCOPE_REQUEST).toString(); template.header("Authorization", token); } } // 2. 配置到 Feign 客户端 @FeignClient(name = "user-service", configuration = HeaderForwardInterceptor.class) public interface UserFeignClient { // ... } // 3. 异步调用前传递关键头信息 public void asyncCall() { String authHeader = getCurrentAuthHeader(); // 获取线程请求头 executor.submit(() -> { // 手动设置到子线程 RequestContextHolder.getRequestAttributes().setAttribute( "Authorization", authHeader, RequestAttributes.SCOPE_REQUEST); userFeignClient.getUserById(1); }); } ``` --- #### 方案 4:集成 Sleuth 传递 TraceID(分布式追踪) 如果使用 **Spring Cloud Sleuth**,可通过 `TraceableExecutorService` 自动传递 TraceID 和 SpanID: ```yaml # 1. 添加 Sleuth 依赖(自动传递 TraceID) <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> # 2. 配置 Traceable 线程池 @Bean public Executor traceableExecutor(Tracer tracer) { return new LazyTraceThreadPoolTaskExecutor(beanFactory, Executors.newFixedThreadPool(10)); } # 3. 使用线程池 @Async("traceableExecutor") public CompletableFuture<User> asyncCall() { // Feign 调用会自动携带 TraceID return CompletableFuture.completedFuture(userFeignClient.getUserById(1)); } ``` --- ### 最佳实践总结 | 场景 | 解决方案 | 优点 | |--| | 简单异步任务 | 手动复制 `RequestContextHolder` | 代码直观,无需额外依赖 | | 高频异步调用 | 装饰线程池传递上下文 | 复用性强,减少重复代码 | | 需要精准控制请求头 | Feign 拦截器 + 显式传参 | 灵活性高,可定制关键头信息 | | 分布式追踪系统 | Sleuth + `TraceableExecutor` | 自动传递 TraceID,集成链路追踪 | --- ### 注意事项 1. **线程池隔离**:避免使用默认的 `ForkJoinPool`,需配置独立线程池防止资源竞争。 2. **上下文清理**:务必在异步任务结束后调用 `resetRequestAttributes()`,避免内存泄漏。 3. **敏感信息**:不要传递安全凭证等敏感头信息到不可信线程池。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值