上線前一個小時,dubbo這個問題可把我折騰慘了

前因

那是一個月黑風高的夜晚,無論有沒有圓圓的月亮,都沒法解救要加班的我。這就是苦澀的人生啊!java

那天正好是春節回家的日子,定了晚上的票,而後仍是上線的日子。git

測試在作迴歸測試的時候,發現一個老功能報錯了,什麼鬼,都沒改過那塊代碼怎麼會出問題?案件疑點重重呀。。。github

爲了可以早點上線,早點回家,因此這個Bug就顯得十萬火急了,由於就這一個問題,其餘都沒問題,解決好了就能夠上線了,因而開啓了破案之路。apache

第一步:找到錯誤信息

機智的我在第一時間打開了Cat查看具體的錯誤,因爲當時並無想到去寫一篇文章出來,錯誤信息也就沒有截圖,後面經過模擬的操做,獲得了相似的同樣的錯誤信息以下:api

圖片

竟然是類轉換錯誤,點進去查看詳細的錯誤信息,以下圖:bash

圖片

真正有價值的錯誤信息以下:微信

dubbo version: 2.7.3, current host: 192.168.8.224 java.lang.ClassCastException: java.util.HashMap cannot be cast to com.cxytiandi.kittycloud.user.api.request.Address
複製代碼

第二步:排查報錯的代碼

公司代碼不方便透露,下面都是模擬的代碼:測試

public ResponseData<String> login(UserLoginRequest loginRequest) {
    loginRequest.getAddress().stream().map(a -> a.getStatus()).collect(Collectors.toList());
    return Response.ok("xxxxxxxxx");
}
複製代碼

問題就出在了map這裏,從loginRequest參數中獲取address是一個Listui

,Address中有status字段,若是是正常的對象沒有問題,錯誤告訴咱們是HashMap不能轉換成Address類,也就是說參數中的Address變成了HashMap致使的錯誤。

參數代碼:spa

@Data
public class UserLoginRequest implements Serializable {
    private String username;
    private String pass;
    private List<Address> address;
}
@Data
@AllArgsConstructor
public class Address implements Serializable {
    private int status;
}
複製代碼

第三步:本地復現錯誤

找到錯誤後,立刻本地啓動相關的兩個服務,咱們分別叫A和B吧,現象是A調用B的某個RPC接口報錯。

本地啓動後立刻復現了錯誤,在報錯的地方打斷點看參數是否變成了HashMap,果不其然,以下圖:

圖片

到這裏感受有點懵,參數中明明是具體的對象類型,怎麼忽然就變成了HashMap,匪夷所思。

而後想着是否是在上層什麼地方出問題了,繼續查看報錯的上層代碼,沒有發現異常。而後決定在PRC的入口處打個斷點看看是否是參數一過來就出問題了,最後通過驗證確實如此,也就排除了B服務中對參數作了轉換。

接着再看下Dubbo內部的參數解碼,

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)。也就是請求到達B以後解碼出來的已是HashMap了,那麼問題確定是調用方傳輸的參數有問題。

圖片

第四步:排查調用方代碼

在調用方這邊發起請求前,查看了參數對象,發現這個時候參數已經出問題了,字段類型發生了變化,因此問題就出在這裏,都是老代碼,應該都沒改過,而是事實卻被改了,經過Idea的Annotate快速的查看了當前方法中有被修改的記錄,找到了修改的代碼,下面經過模擬的方式貼出有問題的代碼,以下:

@Reference(version = DubboConstant.VERSION_V100, group = DubboConstant.DEFAULT_GROUP)
private UserRemoteService userRemoteService;
public void test() {
    UserLoginRequest request = new UserLoginRequest();
    request.setUsername("yjh");
    request.setPass("123456");
    List<Address> address = new ArrayList<>();
    address.add(new Address(1));
    request.setAddress(address);
    UserLoginRequest2 request2 = new UserLoginRequest2();
    request2.setUsername("yjh2");
    request2.setPass("1234562");
    List<Address2> address2 = new ArrayList<>();
    address2.add(new Address2(StatusEnum.INVALID));
    request2.setAddress(address2);
    
    BeanUtils.copyProperties(request2, request);
    
    userRemoteService.login(request);
}
複製代碼

出問題的就是BeanUtils.copyProperties(request2, request); 這行代碼,將一個對象複製到另外一個對象,兩個對象的屬性都同樣,惟一不同的是Address中的status是int類型,Address2中的status是Enum,複製過去就出問題了。

圖片

這種狀況也只在Dubbo的RPC請求出問題,若是是Http請求,基本類型變成了枚舉,直接就報錯了,沒法轉換。

圖片

第五步:BeanUtils問題排查

歸根到底仍是copy的問題,我作了個小實驗,若是是Address2 copy到Address 是不會出問題的,只有嵌套的對象纔會出問題。

特地看了下copy的代碼,若是是Address2 copy到Address,那麼就是status到status,在copy以前會進行判斷Address的setStatus的第一個參數類型和Address2的getStatus的返回值是否相同,若是相同纔會進行賦值操做,不一樣就不會,若是是單個對象在這裏就會直接過濾掉了,一個是int一個是Enum。

圖片

嵌套對象之因此能夠那是由於address的參數和返回類型都是List,沒有去判斷嵌套類裏面的,是整個集合直接複製賦值的,下圖是目標方法:

圖片

value是新的集合對象,invoke後整個address就變了。

圖片

第六步:Dubbo解碼問題排查

前面分析中,調用以前經過BeanUtils複製,只是將枚舉賦值給了基本類型,若是Dubbo在接收到參數進行解碼時可以識別出類型不一致,這樣就直接會報錯了,然而並無,特地調試了下Dubbo解碼的代碼,默認是Hessian的解碼,懷疑跟Hessian有關,因而我把序列化改爲了FastJson,在解碼參數的時候就直接報錯了,不能轉換成int類型。而Hessian在映射不了的時候就直接變成HashMap了,這纔有了咱們前面的錯誤。

圖片

結局

找到緣由後解決就是分分鐘的事了,經過這個問題仍是說明了加任何的代碼都有風險。剩下的就是開發的鍋了,加了代碼沒有自測,好在有測試把關,不然就涼涼了。

感興趣的能夠關注下個人微信公衆號 猿天地,更多技術文章第一時間閱讀。個人GitHub也有一些開源的代碼 github.com/yinjihuan

相關文章
相關標籤/搜索