那是一個月黑風高的夜晚,無論有沒有圓圓的月亮,都沒法解救要加班的我。這就是苦澀的人生啊!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請求,基本類型變成了枚舉,直接就報錯了,沒法轉換。
歸根到底仍是copy的問題,我作了個小實驗,若是是Address2 copy到Address 是不會出問題的,只有嵌套的對象纔會出問題。
特地看了下copy的代碼,若是是Address2 copy到Address,那麼就是status到status,在copy以前會進行判斷Address的setStatus的第一個參數類型和Address2的getStatus的返回值是否相同,若是相同纔會進行賦值操做,不一樣就不會,若是是單個對象在這裏就會直接過濾掉了,一個是int一個是Enum。
嵌套對象之因此能夠那是由於address的參數和返回類型都是List,沒有去判斷嵌套類裏面的,是整個集合直接複製賦值的,下圖是目標方法:
value是新的集合對象,invoke後整個address就變了。
前面分析中,調用以前經過BeanUtils複製,只是將枚舉賦值給了基本類型,若是Dubbo在接收到參數進行解碼時可以識別出類型不一致,這樣就直接會報錯了,然而並無,特地調試了下Dubbo解碼的代碼,默認是Hessian的解碼,懷疑跟Hessian有關,因而我把序列化改爲了FastJson,在解碼參數的時候就直接報錯了,不能轉換成int類型。而Hessian在映射不了的時候就直接變成HashMap了,這纔有了咱們前面的錯誤。
找到緣由後解決就是分分鐘的事了,經過這個問題仍是說明了加任何的代碼都有風險。剩下的就是開發的鍋了,加了代碼沒有自測,好在有測試把關,不然就涼涼了。
感興趣的能夠關注下個人微信公衆號 猿天地,更多技術文章第一時間閱讀。個人GitHub也有一些開源的代碼 github.com/yinjihuan