您好,登錄后才能下訂單哦!
微服務(wù)調(diào)用應(yīng)答返回時(shí)報(bào)ClassCastException問題的實(shí)例分析,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
問題復(fù)現(xiàn)demo在這里。
前幾天被拉去看一個(gè)問題。某服務(wù)(后面稱其為A服務(wù))采用同步模式運(yùn)行,RPC方式調(diào)用其他微服務(wù)。在本地調(diào)試無問題,線上運(yùn)行時(shí)此服務(wù)調(diào)用另外一個(gè)服務(wù)(后面稱其為B服務(wù))的接口會(huì)報(bào)錯(cuò),且通過他們自定義擴(kuò)展的一個(gè)HttpClientFilter
的日志來看,被調(diào)用的provider服務(wù)已經(jīng)正常返回了應(yīng)答消息,但是在后面會(huì)報(bào)ClassCastException
,無法將InvocationException
轉(zhuǎn)型為業(yè)務(wù)代碼的返回值類型。日志如下:
// 業(yè)務(wù)邏輯被調(diào)用 [INFO] test() is called! com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:20) // 用戶自定義的HttpClientFilter中打印了provider返回的消息 [INFO] get response, status[200], content is [{"content":"returnOK"}] com.github.yhs0092.blogdemo.javachassis.filter.PrintResponseFilter.afterReceiveResponse(PrintResponseFilter.java:26) // ClassCastException被拋出 [ERROR] invoke failed, invocation=PRODUCER rest client.consumer.test org.apache.servicecomb.swagger.invocation.exception.DefaultExceptionToResponseConverter.convert(DefaultExceptionToResponseConverter.java:35) java.lang.ClassCastException: org.apache.servicecomb.swagger.invocation.exception.InvocationException cannot be cast to com.github.yhs0092.blogdemo.javachassis.service.TestResponse at com.sun.proxy.$Proxy30.test(Unknown Source) at com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:21) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.doInvoke(SwaggerProducerOperation.java:160) at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.syncInvoke(SwaggerProducerOperation.java:148) at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.invoke(SwaggerProducerOperation.java:115) at org.apache.servicecomb.core.handler.impl.ProducerOperationHandler.handle(ProducerOperationHandler.java:40)
分析問題的過程中,他們提到由于線上的B服務(wù)還是舊版本的沒有升級(jí),于是他們把A服務(wù)依賴的B服務(wù)的接口jar包替換成了低版本來啟動(dòng)的。
初步接觸這個(gè)問題給人一種很怪異的感覺。如果一個(gè)consumer調(diào)用provider時(shí)都已經(jīng)拿到了應(yīng)答,那么會(huì)直接把應(yīng)答返回給consumer的業(yè)務(wù)邏輯代碼;萬一中間真的出錯(cuò)了,那產(chǎn)生的InvocationException
也應(yīng)該是被“拋”出去的,而不是像日志里面顯示的那樣,嘗試“返回”給consumer的業(yè)務(wù)邏輯才對(duì)。
可供分析的信息太少了,只能回頭看一下sdk代碼的相關(guān)邏輯,看看能不能復(fù)現(xiàn)出這個(gè)問題。
RPC調(diào)用模式的微服務(wù)里,業(yè)務(wù)邏輯通過provider接口做調(diào)用時(shí),實(shí)際是通過ServiceComb生成的provider接口類型的代理來做調(diào)用的。而在這個(gè)代理的背后,實(shí)際調(diào)用流程的源頭在org.apache.servicecomb.provider.pojo.Invoker
類里面。同步調(diào)用模式下,區(qū)分應(yīng)答如何被返回給業(yè)務(wù)邏輯的關(guān)鍵代碼在syncInvoke
方法里:
protected Object syncInvoke(Invocation invocation, SwaggerConsumerOperation consumerOperation) { Response response = InvokerUtils.innerSyncInvoke(invocation); if (response.isSuccessed()) { // 在這里,response內(nèi)的result會(huì)作為正常應(yīng)答返回給業(yè)務(wù)邏輯 return consumerOperation.getResponseMapper().mapResponse(response); } // 這里是異常邏輯,response內(nèi)的result即為錯(cuò)誤信息,會(huì)被包裝為InvocationException拋給業(yè)務(wù)邏輯 throw ExceptionFactory.convertConsumerException(response.getResult()); }
出現(xiàn)了線上日志中的錯(cuò)誤說明這個(gè)方法沒有走到throw語句,而是走return語句那里返回了。
InvokerUtils.innerSyncInvoke()
方法里觸發(fā)的主要流程是Handler->HttpClientFilter->網(wǎng)絡(luò)線程,既然在用戶自定義的HTTPClientFilter
實(shí)現(xiàn)類的afterReceiveResponse()
方法中已經(jīng)打印出了B服務(wù)返回的應(yīng)答消息,那么網(wǎng)絡(luò)線程部分的嫌疑就可以排除了。問題只可能出在Invoker
、Handler
、HTTPClientFilter
這三塊。這個(gè)異常需要被catch住并塞到response
里。同時(shí),為了讓異常作為response body返回,而不是被“拋”出去,response.isSuccessed()
需要返回true
,這就要求response
的Http狀態(tài)碼必須是2xx的。通過在demo中加入自定義的HttpClientFilter
,在afterReceiveResponse()
方法中拋出一個(gè)狀態(tài)碼為200的InvocationException
,我們復(fù)現(xiàn)出了這個(gè)問題,其日志特征與A服務(wù)的線上日志一致。
一個(gè)response,里面裝著一個(gè)異常,Http狀態(tài)碼卻是2xx的,這個(gè)場(chǎng)景應(yīng)該是不會(huì)發(fā)生的才對(duì)。在向A服務(wù)的開發(fā)同學(xué)確認(rèn)了他們沒有在自定義的Handler
、HttpClientFilter
內(nèi)直接操作response后,我們通過日志也無法給出問題結(jié)論,只能等A服務(wù)的開發(fā)同學(xué)本地復(fù)現(xiàn)問題場(chǎng)景了。
好在這個(gè)問題本地是能夠復(fù)現(xiàn)出來的,根因是在于A服務(wù)依賴的B服務(wù)接口jar包被替換后,舊版本的業(yè)務(wù)接口應(yīng)答類型比新版本的多一個(gè)屬性,而且這個(gè)屬性的類型是找不到的,大致像下面這樣:
class ResponseType { private InnerFieldType someField; // 這里的InnerFieldType會(huì)報(bào)ClassNotFound }
于是當(dāng)DefaultHttpClientFilter
的extractResult()
方法嘗試將Http body中的json串反序列化為業(yè)務(wù)代碼中的應(yīng)答對(duì)象時(shí),會(huì)拋出一個(gè)異常,而這個(gè)異常被包裝成InvocationException
后,是被“return”回去的,而不是“throw”出去的,并且這個(gè)過程中沒有打印任何日志。關(guān)鍵代碼在DefaultHttpClientFilter
的85-89行:
try { return produceProcessor.decodeResponse(responseEx.getBodyBuffer(), responseMeta.getJavaType()); } catch (Exception e) { return ExceptionFactory.createConsumerException(e); // 異常被返回 }
“return”回去的異常被作為正常的應(yīng)答對(duì)象塞進(jìn)了response
中,而response
的狀態(tài)碼是Http應(yīng)答的狀態(tài)碼——200,于是就有了線上碰到的錯(cuò)誤。
ServiceComb框架在此次定位過程中暴露出來的缺少日志的問題會(huì)在后續(xù)版本中修復(fù)。但是對(duì)于開發(fā)者而言,更重要的是服務(wù)上線部署前需要做好充分驗(yàn)證,臨時(shí)替換依賴jar包這種簡(jiǎn)單粗暴的處理方式不可取。
那么本地調(diào)試過程中碰到這種問題應(yīng)該如何定位呢?以本文所描述的場(chǎng)景(RPC調(diào)用方式,同步運(yùn)行模式)來看,當(dāng)業(yè)務(wù)代碼中觸發(fā)一次微服務(wù)調(diào)用,ServiceComb的處理流程大致是:
Invoker -> InvokerUtils -> Handler -> HTTPClientFilter -> 網(wǎng)絡(luò)線程
Invoker
是RPC調(diào)用模式下的動(dòng)態(tài)代理,業(yè)務(wù)代碼通過provider接口做調(diào)用時(shí),參數(shù)首先被傳到invoke()
方法中。由于consumer工作于同步模式,Invoker
會(huì)通過syncInvoke()
方法調(diào)用InvokerUtils
的innerSyncInvoke()
方法。在這里,Invocation
的next()
方法被調(diào)用,從而觸發(fā)Handler鏈執(zhí)行。在Handler鏈的末尾是TransportClientHandler
,它會(huì)調(diào)用對(duì)應(yīng)的transport方式發(fā)送請(qǐng)求。在Rest over Vertx傳輸方式下,我們需要關(guān)注的是RestClientInvocation
的invoke()
方法,這里會(huì)遍歷執(zhí)行HttpClientFilter的beforeSendRequest()方法,然后將請(qǐng)求調(diào)度到網(wǎng)絡(luò)線程中發(fā)送。業(yè)務(wù)線程此時(shí)處于等待返回的狀態(tài)(SyncResponseExecutor.waitResponse()
方法中使用CountDownLatch
進(jìn)行等待)。
當(dāng)請(qǐng)求應(yīng)答返回后,RestClientInvocation.processResponseBody()
方法會(huì)將Http response body返回給業(yè)務(wù)線程處理(通過觸發(fā)SyncResponseExecutor
的CountDownLatch
)。應(yīng)答首先會(huì)在RestClientInvocation
中遍歷HttpClientFilter的afterReceiveResponse()方法進(jìn)行處理,然后經(jīng)過Handler鏈的回調(diào)處理,最終返回給InvokerUtils
的syncInvoke()
方法。其中,Http response body是在DefaultHttpClientFilter的extractResult()方法中反序列化為業(yè)務(wù)接口返回對(duì)象的。這個(gè)方法會(huì)根據(jù)response
的HTTP狀態(tài)碼判斷如何對(duì)待結(jié)果,如果是2xx的狀態(tài)碼,則response
中的result
會(huì)作為正常的應(yīng)答返回給業(yè)務(wù)邏輯,否則會(huì)將result
包裝到InvocationException
中拋給業(yè)務(wù)邏輯。
// RestClientInvocation中處理應(yīng)答的關(guān)鍵方法 protected void processResponseBody(Buffer responseBuf) { invocation.getResponseExecutor().execute(() -> { // 同步模式下,應(yīng)答返回流程從這里開始就是在業(yè)務(wù)線程里執(zhí)行的 try { HttpServletResponseEx responseEx = new VertxClientResponseToHttpServletResponse(clientResponse, responseBuf); for (HttpClientFilter filter : httpClientFilters) { // HttpClientFilter處理返回消息體,普通的filter會(huì)返回null Response response = filter.afterReceiveResponse(invocation, responseEx); if (response != null) { // DefaultHttpClientFilter會(huì)把消息體反序列化為應(yīng)答對(duì)象,裝入response返回 asyncResp.complete(response); // 通過回調(diào)觸發(fā)handler鏈 return; } } } catch (Throwable e) { asyncResp.fail(invocation.getInvocationType(), e); // 包裝異常,通過回調(diào)觸發(fā)handler鏈 } }); }
本地分析這類問題的時(shí)候,首先需要知道請(qǐng)求發(fā)送的流程,了解RPC動(dòng)態(tài)代理的入口、Handler
鏈的起止點(diǎn)、HttpClientFilter
的調(diào)用點(diǎn)。這些是流程中的關(guān)鍵節(jié)點(diǎn),根據(jù)這些信息可以大致確定問題出現(xiàn)的范圍。至于更進(jìn)一步的定位,就需要大家根據(jù)具體的問題進(jìn)行分析了。
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。