解决SpringCloudfeignGET请求无法用实体传参的问题

Malina ·
更新时间:2024-09-20
· 1039 次阅读

目录

Spring Cloud feign GET请求无法用实体传参

解决办法

Spring Cloud Feign异步调用传参问题

方案1

方案2

总结

Spring Cloud feign GET请求无法用实体传参

代码如下:

@FeignClient(name = "eureka-client", fallbackFactory = FallBack.class, decode404 = true, path = "/client") public interface FeignApi { //    @PostMapping("/hello/{who}") //    String hello(@PathVariable(value = "who") String who) throws Exception;     @GetMapping("/hello")     String hello(Params params) throws Exception; }

调用报错:

feign.FeignException: status 405 reading FeignApi#hello(Params)

解决办法

改用post请求,添加@RequestBodey注解

新增@SpringQueryMaq注解,如下:

@GetMapping("/hello") String hello(@SpringQueryMap Params params) throws Exception; Spring Cloud Feign异步调用传参问题

各个子系统之间通过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();         });     } }

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

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

@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);         }

问题得以圆满解决。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持软件开发网。



实体

需要 登录 后方可回复, 如果你还没有账号请 注册新账号