背景

各个子系统之间通过feign调用,每个服务提供方需要验证每个请求header里的token

public void invokeFeign() throws Exception {
    feignService1.method();
    feignService2.method();
    feignService3.method();
....
}

定义拦截每次发送feign调用拦截器RequestInterceptor的子类,每次发送feign请求前将token带入请求头

@Configuration
public class FeignTokenInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        public void apply(RequestTemplate template) {
            //上下文环境保持器,拿到刚进来这个请求包含的数据,而不会因为远程数据请求头被清除
            ServletRequestAttributes attributes = (ServletRequestAttributes)                  RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();//老的请求
            if (request != null) {
                //同步老的请求头中的数据,这里是获取cookie
                String cookie = request.getHeader("token");
                template.header("token", cookie);
            }
        }
  .....
    }

这样便能实现系统间通过同步方式feign调用的认证问题。但是如果需要在invokeFeign方法中feignService3的方法调用比较耗时,并且invokeFeign业务并不关心feignService3.method()方法的执行结果,此时该怎么办。

方案1:

修改feignService3.method()方法,将其内部实现修改为异步,这种方案依赖服务的提供方,如果feignService3服务是其他业务部门维护,并且无法修改实现为异步,此时只能采取方案2.

方案2:

通过线程池调用feignServie3.method()

public void invokeFeign() throws Exception {
    feignService1.method();
    feignService2.method();
    executor.submit(()->{
        feignService3.method();
    });
....
}

怀着期待的心情开启了尝试,你会发现调用feignService3方法并没有成功,查看日志你将会发现是由于feign发送request请求的header中未携带token导致。于是百度了下feign异步调用传参,网上大部分的解决方案,如下

public void invokeFeign() throws Exception {
        feignService1.method();
        feignService2.method();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        executor.submit(()->{
            RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
            feignService3.method();
        });
    }
}

添加了上面的代码后,实测无效,此时确实有些束手无策。但是真的没无效吗?我仔细比对通过上述手段解决问题的博客,他们的业务代码和我的代码不同之处。确实有不同,比如https://www.jb51.net/article/249407.htm这篇。其代码如下

@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
    //从主线程中获得所有request数据
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
        //1、远程查询所有地址列表
        RequestContextHolder.setRequestAttributes(requestAttributes);
        List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
        confirmVo.setAddress(address);
    }, executor);

    //2、远程查询购物车所选的购物项,获得所有购物项数据
    CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
        //放入子线程中request数据
        RequestContextHolder.setRequestAttributes(requestAttributes);
        List<OrderItemVo> items = cartFeginService.getCurrentUserCartItems();
        confirmVo.setItem(items);
    }, executor).thenRunAsync(()->{
        RequestContextHolder.setRequestAttributes(requestAttributes);
        List<OrderItemVo> items = confirmVo.getItem();
        List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
        //远程调用查询是否有库存
        R hasStock = wmsFeignService.getSkusHasStock(collect);
        //形成一个List集合,获取所有物品是否有货的情况
        List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
        });
        if (data!=null){
            //收集起来,Map<Long,Boolean> stocks;
            Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
            confirmVo.setStocks(map);
        }
    },executor);
    //feign远程调用在调用之前会调用很多拦截器,因此远程调用会丢失很多请求头

    //3、查询用户积分
    Integer integration = memberResVo.getIntegration();
    confirmVo.setIntegration(integration);
    //其他数据自动计算

    CompletableFuture.allOf(getAddressFuture,cartFuture).get();
    return confirmVo;
}

我们看的出来,他的业务代码即使是开启多线程,也是等最后线程里的任务都执行完成后,业务方法才结束返回,而我的业务方法并不会等feignService3调用完成结束,抱着尝试的心态,我调整了下代码添加了CountDownLatch,让业务方法等待feign调用结束后在返回。

public void invokeFeign() throws Exception {
        feignService1.method();
        feignService2.method();
        CountDownLatch latch = new CountDownLatch(1);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        executor.submit(()->{
            RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
            feignService3.method();
            latch.countDown();
        });
        latch.await();
    }
}

不如所料,调用成功了。到这里看似是解决了问题,但是与我想象的异步差别太大了,最终业务线程还是需要等待feignService3.method()调用业务方法才能返回,而且异步场景如发送短信、消息推送,记录日志可能调用耗时,业务方法可不想等待他们执行结束,此时该怎么解决?只能翻源码ServletRequestAttributes.java

首先看到了注释,这给了我灵感

Servlet-based implementation of the {@link RequestAttributes} interface. <p>Accesses objects from servlet request and HTTP session scope,
with no distinction between "session" and "global session".

servlet请求和HTTP会话范围访问对象,"session"和"global session"作用域没有区别。对呀会不会是因为header中的参数是request作用域的原因呢,因为请求结束,所以即使在子线程设置请求头,也取不到原因。回到请求拦截器RequestInterceptor查看获取token地方

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    //老的请求
    HttpServletRequest request = attributes.getRequest();
if (request != null) {
        //同步老的请求头中的数据,这里是获取cookie
        String cookie = request.getHeader("token");
        template.header("token", cookie);
        }

果然如此,从attributes中获取request,然后从request中获取token。但是没有考虑到request请求结束,request作用域的问题,此时肯定取不到header里的token了。

那么该怎么解决呢?思路不能变,肯定还是围绕着ServletRequestAttributes展开,发现他有两个方法getAttributessetAttribute,而且这俩方法都支持两个作用域requestsession

@Override
public Object getAttribute(String name, int scope) {
    if (scope == SCOPE_REQUEST) {
        if (!isRequestActive()) {
            throw new IllegalStateException(
                    "Cannot ask for request attribute - request is not active anymore!");
        }
        return this.request.getAttribute(name);
    }
    else {
        HttpSession session = getSession(false);
        if (session != null) {
            try {
                Object value = session.getAttribute(name);
                if (value != null) {
                    this.sessionAttributesToUpdate.put(name, value);
                }
                return value;
            }
            catch (IllegalStateException ex) {
                // Session invalidated - shouldn't usually happen.
            }
        }
        return null;
    }
}

@Override
public void setAttribute(String name, Object value, int scope) {
    if (scope == SCOPE_REQUEST) {
        if (!isRequestActive()) {
            throw new IllegalStateException(
                    "Cannot set request attribute - request is not active anymore!");
        }
        this.request.setAttribute(name, value);
    }
    else {
        HttpSession session = obtainSession();
        this.sessionAttributesToUpdate.remove(name);
        session.setAttribute(name, value);
    }
}

既然我们的业务方法调用(HttpServletRequest)不会等待feignService3.method,我们可以通过ServletRequestAttributes.setAttributes指定作用域为session呀。此时invokeFeign代码如下

public void invokeFeign() throws Exception {
        feignService1.method();
        feignService2.method();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        //在ServeletRequestAttributes中设置token,作用域为session                 
        attributes.setAttribute("token",attributes.getRequest().getHeader("token"),1);
        executor.submit(()->{
            RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
            feignService3.method();
        });
    }
}

然后RequestInterceptor.apply方法也做响应调整,如下

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    //老的请求
    HttpServletRequest request = attributes.getRequest();
    String token = (String) attributes.getAttribute("token",1);
template.header("token",token);
        if (request != null) {
        //同步老的请求头中的数据,这里是获取cookie
        String cookie = request.getHeader("token");
        template.header("token", cookie);
        }

问题得以圆满解决。

到此这篇关于SpringCloud Feign异步调用传参问题的文章就介绍到这了,更多相关SpringCloud Feign传参内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

解决SpringCloud Feign异步调用传参问题的更多相关文章

  1. SpringCloud超详细讲解微服务网关Zuul基础

    这篇文章主要介绍了SpringCloud Zuul微服务网关,负载均衡,熔断和限流,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. SpringCloud gateway+zookeeper实现网关路由的详细搭建

    这篇文章主要介绍了SpringCloud gateway+zookeeper实现网关路由,本文通过图文实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. SpringCloud OpenFeign 服务调用传递 token的场景分析

    这篇文章主要介绍了SpringCloud OpenFeign 服务调用传递 token的场景分析,本篇文章简单介绍 OpenFeign 调用传递 header ,以及多线程环境下可能会出现的问题,其中涉及到 ThreadLocal 的相关知识,需要的朋友可以参考下

  4. Spring Cloud超详细i讲解Feign自定义配置与使用

    这篇文章主要介绍了SpringCloud Feign自定义配置与使用,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. Springcloud Stream消息驱动工具使用介绍

    SpringCloud Stream由一个中间件中立的核组成,应用通过SpringCloud Stream插入的input(相当于消费者consumer,它是从队列中接收消息的)和output(相当于生产者producer,它是发送消息到队列中的)通道与外界交流

  6. SpringCloud中Gateway的使用教程详解

    SpringCloud Gateway是Spring体系内的一个全新项目,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。本文就来为大家详细讲讲Gateway的使用教程,需要的可以参考一下

  7. 浅谈react路由传参的几种方式

    这篇文章主要介绍了浅谈react路由传参的几种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  8. SpringCloud 客户端Ribbon负载均衡的实现方法

    Ribbon 是 Netflix 提供的一个基于 Http 和 TCP 的客户端负载均衡工具,且已集成在 Eureka 依赖中,这篇文章主要介绍了SpringCloud 客户端Ribbon负载均衡的实现方法,需要的朋友可以参考下

  9. SpringCloud服务网关Gateway的使用教程详解

    SpringCloud Gateway是Spring体系内的一个全新项目,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。本文就来为大家详细讲讲Gateway的使用教程,需要的可以参考一下

  10. SpringCloud 分布式微服务架构操作步骤

    SpringCloud是一种微服务的框架,利用它我们可以去做分布式服务开发,这篇文章主要介绍了SpringCloud 分布式微服务架构,需要的朋友可以参考下

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部