项目总结|Feign远程调用和异步调用丢失请求头问题

前言

最近一直在梳理之前做过的项目,想到之前遇到过的一个问题,场景是这样的,在我提交订单时,需要查询用户的地址信息和购物车被勾选的购物项,这样的话,我需要调用两个服务,一个是会员服务,一个是购物车服务。由于用户登陆信息是在整个系统共享的(这里采用分布式session解决),所以我在提交订单的时,调用购物车服务的时候,购物车服务的拦截器会拦截请求,判断用户是否登录。这时候请求头丢失,导致购物车服务拦截器返回用户未登录,但实际上是已经登录过的。还有一个问题是异步调用的时候,老请求线程不共享的问题,导致我业务中获取不到老请求报空指针异常的问题

远程调用出现的问题及解决方案

在分布式项目中,发送请求大致就两种,一种是浏览器访问,第二种是服务与服务之间通过OpenFeign远程调用。浏览器发送请求时,它会带上请求头的信息的,所以不会导致cookie丢失,这样用户真实登录的情况下不会判断未登录的异常情况。深入源码发现,Feign会重新创建一个request,这个请求是没有任何请求头的,这个请求模板会遍历请求拦截器的apply方法来丰富这个请求模板
在这里插入图片描述
看到这个地方就有办法解决了,解决方案就是,我写了一个feign拦截器,这里面注入了一个RequestInterceptor的对象,它是一个接口,我重写了它的apply方法,在里面拿到老请求中的请求头信息,放到这个新的请求模板里,我这里更新的是cookie。来看下代码:

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上来,进行一个同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

这样每次进行feign远程调用的时候都需要走到这里丰富一下请求模板,带上cookie,这样用户登录信息就能正常的判断已登录问题。
这个问题就得到解决了。我画了个整体的流程图:
在这里插入图片描述

异步调用出现的问题及解决方案

前面已经说了场景了,我这个service里有两个查询任务,一个是去会员服务中查询会员的详细地址信息,一个是购物车服务中查询购物车勾选的购物项信息,这里我做了步优化,将这两个任务丢到我在这个模块下创建的线程池里,让其异步处理,这里用到CompletableFuture异步编排的功能,这样吞吐量就会有所提升。但是问题来了,在我获取老请求的时候,出现空指针的问题,我们来看看获取老请求是怎么获取的。
在这里插入图片描述
再深入源码看看,这个RequestContextHolder是怎么存储的请求数据

private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
@Nullable
public static RequestAttributes getRequestAttributes() {
    RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
    if (attributes == null) {
        attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
    }

    return attributes;
}

这个requestAttributesHolder其实就是个ThreadLocal,ThreadLocal是线程内部共享的,但是跨线程就是完全不同的数据了,我这里就是,当我将两个任务丢到线程池中创建其他的线程执行的话,它是拿不到我主线程的请求信息的,所以就导致了获取值为空的现象。
我是这么来解决的,来看下我的代码

 /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});

            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }

在这里插入图片描述
每一个线程都来共享之前的这个请求数据。问题得到解决。来画张图来看一下这个整体的问题
在这里插入图片描述

<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. **敏感信息**:不要传递安全凭证等敏感头信息到不可信线程池。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值