解決SpringCloud?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ù)代碼和我的代碼不同之處。確實有不同,比如http://www.dbjr.com.cn/article/249407.htm這篇。其代碼如下
@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); }
問題得以圓滿解決。
到此這篇關(guān)于SpringCloud Feign異步調(diào)用傳參問題的文章就介紹到這了,更多相關(guān)SpringCloud Feign傳參內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Spring?Cloud實現(xiàn)日志管理模塊
這篇文章主要介紹了關(guān)于Spring?Cloud實現(xiàn)日志管理模塊問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11Java Comparator.comparing比較導(dǎo)致空指針異常的解決
這篇文章主要介紹了Java Comparator.comparing比較導(dǎo)致空指針異常的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Batch讀取txt文件并寫入數(shù)據(jù)庫的方法教程
這篇文章主要給大家介紹了Spring Batch讀取txt文件并寫入數(shù)據(jù)庫的方法,SpringBatch 是一個輕量級、全面的批處理框架。這里我們用它來實現(xiàn)文件的讀取并將讀取的結(jié)果作處理,處理之后再寫入數(shù)據(jù)庫中的功能。需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04

mybatis-plus之如何根據(jù)數(shù)據(jù)庫主鍵定義字段類型