好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

解决SpringCloud Feign异步调用传参问题

背景

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

?

1

2

3

4

5

6

public void invokeFeign() throws Exception {

     feignService1.method();

     feignService2.method();

     feignService3.method();

....

}

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@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()

?

1

2

3

4

5

6

7

8

public void invokeFeign() throws Exception {

     feignService1.method();

     feignService2.method();

     executor.submit(()->{

         feignService3.method();

     });

....

}

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

?

1

2

3

4

5

6

7

8

9

10

11

public void invokeFeign() throws Exception {

         feignService1.method();

         feignService2.method();

         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder

                 .getRequestAttributes();

         executor.submit(()->{

             RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true );

             feignService3.method();

         });

     }

}

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

@Override

public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

     OrderConfirmVo confirmVo = new OrderConfirmVo();

     MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

     //从主线程中获得所有request数据

     RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

     CompletableFuture&lt;Void&gt; getAddressFuture = CompletableFuture.runAsync(() -&gt; {

         //1、远程查询所有地址列表

         RequestContextHolder.setRequestAttributes(requestAttributes);

         List&lt;MemberAddressVo&gt; address = memberFeignService.getAddress(memberResVo.getId());

         confirmVo.setAddress(address);

     }, executor);

 

     //2、远程查询购物车所选的购物项,获得所有购物项数据

     CompletableFuture&lt;Void&gt; cartFuture = CompletableFuture.runAsync(() -&gt; {

         //放入子线程中request数据

         RequestContextHolder.setRequestAttributes(requestAttributes);

         List&lt;OrderItemVo&gt; items = cartFeginService.getCurrentUserCartItems();

         confirmVo.setItem(items);

     }, executor).thenRunAsync(()-&gt;{

         RequestContextHolder.setRequestAttributes(requestAttributes);

         List&lt;OrderItemVo&gt; items = confirmVo.getItem();

         List&lt;Long&gt; collect = items.stream().map(item -&gt; item.getSkuId()).collect(Collectors.toList());

         //远程调用查询是否有库存

         R hasStock = wmsFeignService.getSkusHasStock(collect);

         //形成一个List集合,获取所有物品是否有货的情况

         List&lt;SkuStockVo&gt; data = hasStock.getData( new TypeReference&lt;List&lt;SkuStockVo&gt;&gt;() {

         });

         if (data!= null ){

             //收集起来,Map&lt;Long,Boolean&gt; stocks;

             Map&lt;Long, Boolean&gt; 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 调用结束后在返回。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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

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

?

1

2

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 地方

?

1

2

3

4

5

6

7

8

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 展开,发现他有两个方法 getAttributes 和 setAttribute ,而且这俩方法都支持两个作用域 request 、 session 。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

@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 代码如下

?

1

2

3

4

5

6

7

8

9

10

11

12

13

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 方法也做响应调整,如下

?

1

2

3

4

5

6

7

8

9

10

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传参内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

原文链接:https://juejin.cn/post/7101492042898866184

查看更多关于解决SpringCloud Feign异步调用传参问题的详细内容...

  阅读:17次