解決Spring Cloud feign GET請求無法用實體傳參的問題
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); }
問題得以圓滿解決。
總結
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Feign調用中的兩種Header傳參方式小結
- Spring Cloud Feign請求添加headers的實現方式
- SpringCloud OpenFeign 服務調用傳遞 token的場景分析
- java開發web前端cookie session及token會話機制詳解
- 使用springcloud+oauth2攜帶token去請求其他服務