• SpringCloud Feign异步调用传参问题


    背景

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

    1. public void invokeFeign() throws Exception {
    2. feignService1.method();
    3. feignService2.method();
    4. feignService3.method();
    5. ....
    6. }

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

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

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

    方案1:

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

    方案2:

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

    1. public void invokeFeign() throws Exception {
    2. feignService1.method();
    3. feignService2.method();
    4. executor.submit(()->{
    5. feignService3.method();
    6. });
    7. ....
    8. }

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

    1. public void invokeFeign() throws Exception {
    2. feignService1.method();
    3. feignService2.method();
    4. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
    5. .getRequestAttributes();
    6. executor.submit(()->{
    7. RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
    8. feignService3.method();
    9. });
    10. }
    11. }

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

    1. @Override
    2. public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    3. OrderConfirmVo confirmVo = new OrderConfirmVo();
    4. MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
    5. //从主线程中获得所有request数据
    6. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    7. CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
    8. //1、远程查询所有地址列表
    9. RequestContextHolder.setRequestAttributes(requestAttributes);
    10. List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
    11. confirmVo.setAddress(address);
    12. }, executor);
    13. //2、远程查询购物车所选的购物项,获得所有购物项数据
    14. CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
    15. //放入子线程中request数据
    16. RequestContextHolder.setRequestAttributes(requestAttributes);
    17. List<OrderItemVo> items = cartFeginService.getCurrentUserCartItems();
    18. confirmVo.setItem(items);
    19. }, executor).thenRunAsync(()->{
    20. RequestContextHolder.setRequestAttributes(requestAttributes);
    21. List<OrderItemVo> items = confirmVo.getItem();
    22. List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
    23. //远程调用查询是否有库存
    24. R hasStock = wmsFeignService.getSkusHasStock(collect);
    25. //形成一个List集合,获取所有物品是否有货的情况
    26. List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
    27. });
    28. if (data!=null){
    29. //收集起来,Map<Long,Boolean> stocks;
    30. Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
    31. confirmVo.setStocks(map);
    32. }
    33. },executor);
    34. //feign远程调用在调用之前会调用很多拦截器,因此远程调用会丢失很多请求头
    35. //3、查询用户积分
    36. Integer integration = memberResVo.getIntegration();
    37. confirmVo.setIntegration(integration);
    38. //其他数据自动计算
    39. CompletableFuture.allOf(getAddressFuture,cartFuture).get();
    40. return confirmVo;
    41. }

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

    1. public void invokeFeign() throws Exception {
    2. feignService1.method();
    3. feignService2.method();
    4. CountDownLatch latch = new CountDownLatch(1);
    5. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
    6. .getRequestAttributes();
    7. executor.submit(()->{
    8. RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
    9. feignService3.method();
    10. latch.countDown();
    11. });
    12. latch.await();
    13. }
    14. }

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

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

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

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

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

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

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

    1. @Override
    2. public Object getAttribute(String name, int scope) {
    3. if (scope == SCOPE_REQUEST) {
    4. if (!isRequestActive()) {
    5. throw new IllegalStateException(
    6. "Cannot ask for request attribute - request is not active anymore!");
    7. }
    8. return this.request.getAttribute(name);
    9. }
    10. else {
    11. HttpSession session = getSession(false);
    12. if (session != null) {
    13. try {
    14. Object value = session.getAttribute(name);
    15. if (value != null) {
    16. this.sessionAttributesToUpdate.put(name, value);
    17. }
    18. return value;
    19. }
    20. catch (IllegalStateException ex) {
    21. // Session invalidated - shouldn't usually happen.
    22. }
    23. }
    24. return null;
    25. }
    26. }
    27. @Override
    28. public void setAttribute(String name, Object value, int scope) {
    29. if (scope == SCOPE_REQUEST) {
    30. if (!isRequestActive()) {
    31. throw new IllegalStateException(
    32. "Cannot set request attribute - request is not active anymore!");
    33. }
    34. this.request.setAttribute(name, value);
    35. }
    36. else {
    37. HttpSession session = obtainSession();
    38. this.sessionAttributesToUpdate.remove(name);
    39. session.setAttribute(name, value);
    40. }
    41. }

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

    1. public void invokeFeign() throws Exception {
    2. feignService1.method();
    3. feignService2.method();
    4. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
    5. .getRequestAttributes();
    6. //在ServeletRequestAttributes中设置token,作用域为session
    7. attributes.setAttribute("token",attributes.getRequest().getHeader("token"),1);
    8. executor.submit(()->{
    9. RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
    10. feignService3.method();
    11. });
    12. }
    13. }

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

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

    问题得以圆满解决。

  • 相关阅读:
    elasticsearch 之时间类型
    Flutter_Slider_SliderTheme_滑杆/滑块_渐变色
    黑马瑞吉外卖之新增菜品
    阿里云服务器通用型规格族20个实例规格性能特点和适用场景汇总
    【力扣】292. Nim 游戏
    Flink SQL在线调试功能的实现
    【Android笔记46】Android中如何自定义弹出框样式
    服务器推送有几种方式,分别有什么优缺点
    吴恩达机器学习系列课程笔记——第十二章:支持向量机(Support Vector Machines)
    攻防演练案例讲溯源
  • 原文地址:https://blog.csdn.net/q66562636/article/details/124972633