解決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; }
調(diào)用報錯:
feign.FeignException: status 405 reading FeignApi#hello(Params)
解決辦法
改用post請求,添加@RequestBodey注解
新增@SpringQueryMaq注解,如下:
@GetMapping("/hello") String hello(@SpringQueryMap Params params) throws Exception;
Spring Cloud Feign異步調(diào)用傳參問題
各個子系統(tǒng)之間通過feign調(diào)用,每個服務(wù)提供方需要驗證每個請求header里的token。
public void invokeFeign() throws Exception { ? ? feignService1.method(); ? ? feignService2.method(); ? ? feignService3.method(); .... }
定義攔截每次發(fā)送feign調(diào)用攔截器RequestInterceptor的子類,每次發(fā)送feign請求前將token帶入請求頭
@Configuration public class FeignTokenInterceptor implements RequestInterceptor { ? ? @Override ? ? public void apply(RequestTemplate template) { ? ? ? ? public void apply(RequestTemplate template) { ? ? ? ? ? ? //上下文環(huán)境保持器,拿到剛進(jìn)來這個請求包含的數(shù)據(jù),而不會因為遠(yuǎn)程數(shù)據(jù)請求頭被清除 ? ? ? ? ? ? ServletRequestAttributes attributes = (ServletRequestAttributes) ? ? ? ? ? ? ? ? ?RequestContextHolder.getRequestAttributes(); ? ? ? ? ? ? HttpServletRequest request = attributes.getRequest();//老的請求 ? ? ? ? ? ? if (request != null) { ? ? ? ? ? ? ? ? //同步老的請求頭中的數(shù)據(jù),這里是獲取cookie ? ? ? ? ? ? ? ? String cookie = request.getHeader("token"); ? ? ? ? ? ? ? ? template.header("token", cookie); ? ? ? ? ? ? } ? ? ? ? } ? ..... ? ? }
這樣便能實現(xiàn)系統(tǒng)間通過同步方式feign調(diào)用的認(rèn)證問題。但是如果需要在invokeFeign方法中feignService3的方法調(diào)用比較耗時,并且invokeFeign業(yè)務(wù)并不關(guān)心feignService3.method()方法的執(zhí)行結(jié)果,此時該怎么辦。
方案1
修改feignService3.method()方法,將其內(nèi)部實現(xiàn)修改為異步,這種方案依賴服務(wù)的提供方,如果feignService3服務(wù)是其他業(yè)務(wù)部門維護(hù),并且無法修改實現(xiàn)為異步,此時只能采取方案2.
方案2
通過線程池調(diào)用feignServie3.method()
public void invokeFeign() throws Exception { ? ? feignService1.method(); ? ? feignService2.method(); ? ? executor.submit(()->{ ? ? ? ? feignService3.method(); ? ? }); .... }
懷著期待的心情開啟了嘗試,你會發(fā)現(xiàn)調(diào)用feignService3方法并沒有成功,查看日志你將會發(fā)現(xiàn)是由于feign發(fā)送request請求的header中未攜帶token導(dǎo)致。于是百度了下feign異步調(diào)用傳參,網(wǎng)上大部分的解決方案,如下
public void invokeFeign() throws Exception { ? ? ? ? feignService1.method(); ? ? ? ? feignService2.method(); ? ? ? ? ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder ? ? ? ? ? ? ? ? .getRequestAttributes(); ? ? ? ? executor.submit(()->{ ? ? ? ? ? ? RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); ? ? ? ? ? ? feignService3.method(); ? ? ? ? }); ? ? } }
添加了上面的代碼后,實測無效,此時確實有些束手無策。但是真的沒無效嗎?我仔細(xì)比對通過上述手段解決問題的博客,他們的業(yè)務(wù)代碼和我的代碼不同之處。確實有不同,比如這篇。其代碼如下
@Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { ? ? OrderConfirmVo confirmVo = new OrderConfirmVo(); ? ? MemberResVo memberResVo = LoginUserInterceptor.loginUser.get(); ? ? //從主線程中獲得所有request數(shù)據(jù) ? ? RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ? ? CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { ? ? ? ? //1、遠(yuǎn)程查詢所有地址列表 ? ? ? ? RequestContextHolder.setRequestAttributes(requestAttributes); ? ? ? ? List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId()); ? ? ? ? confirmVo.setAddress(address); ? ? }, executor); ? ? ? //2、遠(yuǎn)程查詢購物車所選的購物項,獲得所有購物項數(shù)據(jù) ? ? CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { ? ? ? ? //放入子線程中request數(shù)據(jù) ? ? ? ? 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()); ? ? ? ? //遠(yuǎn)程調(diào)用查詢是否有庫存 ? ? ? ? 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遠(yuǎn)程調(diào)用在調(diào)用之前會調(diào)用很多攔截器,因此遠(yuǎn)程調(diào)用會丟失很多請求頭 ? ? ? //3、查詢用戶積分 ? ? Integer integration = memberResVo.getIntegration(); ? ? confirmVo.setIntegration(integration); ? ? //其他數(shù)據(jù)自動計算 ? ? ? CompletableFuture.allOf(getAddressFuture,cartFuture).get(); ? ? return confirmVo; }
我們看的出來,他的業(yè)務(wù)代碼即使是開啟多線程,也是等最后線程里的任務(wù)都執(zhí)行完成后,業(yè)務(wù)方法才結(jié)束返回,而我的業(yè)務(wù)方法并不會等feignService3調(diào)用完成結(jié)束,抱著嘗試的心態(tài),我調(diào)整了下代碼添加了CountDownLatch,讓業(yè)務(wù)方法等待feign調(diào)用結(jié)束后在返回。
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(); ? ? } }
不如所料,調(diào)用成功了。到這里看似是解決了問題,但是與我想象的異步差別太大了,最終業(yè)務(wù)線程還是需要等待feignService3.method()調(diào)用業(yè)務(wù)方法才能返回,而且異步場景如發(fā)送短信、消息推送,記錄日志可能調(diào)用耗時,業(yè)務(wù)方法可不想等待他們執(zhí)行結(jié)束,此時該怎么解決?
只能翻源碼 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"作用域沒有區(qū)別。對呀會不會是因為header中的參數(shù)是request作用域的原因呢,因為請求結(jié)束,所以即使在子線程設(shè)置請求頭,也取不到原因?;氐秸埱髷r截器RequestInterceptor查看獲取token地方
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ? ? //老的請求 ? ? HttpServletRequest request = attributes.getRequest(); if (request != null) { ? ? ? ? //同步老的請求頭中的數(shù)據(jù),這里是獲取cookie ? ? ? ? String cookie = request.getHeader("token"); ? ? ? ? template.header("token", cookie); ? ? ? ? }
果然如此,從attributes中獲取request,然后從request中獲取token。但是沒有考慮到request請求結(jié)束,request作用域的問題,此時肯定取不到header里的token了。
那么該怎么解決呢?思路不能變,肯定還是圍繞著ServletRequestAttributes展開,發(fā)現(xiàn)他有兩個方法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); ? ? } }
既然我們的業(yè)務(wù)方法調(diào)用(HttpServletRequest)不會等待feignService3.method,我們可以通過
ServletRequestAttributes.setAttributes指定作用域為session呀。
此時invokeFeign代碼如下
public void invokeFeign() throws Exception { ? ? ? ? feignService1.method(); ? ? ? ? feignService2.method(); ? ? ? ? ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder ? ? ? ? ? ? ? ? .getRequestAttributes(); ? ? ? ? //在ServeletRequestAttributes中設(shè)置token,作用域為session ? ? ? ? ? ? ? ?? ? ? ? ? attributes.setAttribute("token",attributes.getRequest().getHeader("token"),1); ? ? ? ? executor.submit(()->{ ? ? ? ? ? ? RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); ? ? ? ? ? ? feignService3.method(); ? ? ? ? }); ? ? } }
然后RequestInterceptor.apply方法也做響應(yīng)調(diào)整,如下
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ? ? //老的請求 ? ? HttpServletRequest request = attributes.getRequest(); ? ? String token = (String) attributes.getAttribute("token",1); template.header("token",token); ? ? ? ? if (request != null) { ? ? ? ? //同步老的請求頭中的數(shù)據(jù),這里是獲取cookie ? ? ? ? String cookie = request.getHeader("token"); ? ? ? ? template.header("token", cookie); ? ? ? ? }
問題得以圓滿解決。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot 中 inputStream 神秘消失之謎(終破)
這篇文章主要介紹了springboot 中 inputStream 神秘消失之謎,為了能夠把這個問題說明,我們首先需要從簡單的http調(diào)用說起,通過設(shè)置body等一些操作,具體實現(xiàn)代碼跟隨小編一起看看吧2021-08-08基于Springboot一個注解搞定數(shù)據(jù)字典的實踐方案
這篇文章主要介紹了基于Springboot一個注解搞定數(shù)據(jù)字典問題,大致的方向是自定義注解,在序列化的時候進(jìn)行數(shù)據(jù)處理,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06Java開發(fā)druid數(shù)據(jù)連接池maven方式簡易配置流程示例
本篇文章主要為大家介紹了java開發(fā)中druid數(shù)據(jù)連接池maven方式的簡易配置流程示例,文中附含詳細(xì)的代碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10