
17年的時候,因為一時沖動沒把持住,結(jié)合面試題寫了一個系列的Dubbo源碼解析,結(jié)果頗受歡迎。根據(jù)我的面試經(jīng)驗而言,能在簡歷上寫上原理、源碼等關(guān)鍵詞的,都是非常具備核心競爭力的。
上周,一位讀者就和我交流了下面試情況:
把簡歷按照你說的那樣改了下,感覺投了就有面試機(jī)會......
確實,面試的時候把源碼一波分析,絕對能令面試官虎軀一震!然而在一陣前戲過后,不料面試官突然來了句令劇情發(fā)生了反轉(zhuǎn):
“你對Dubbo源碼這么熟悉,那請問你使用的時候有沒有遇到什么坑?”
——毫無準(zhǔn)備的他頓時面臨著唬住了50K、唬不住就只能5K的局面,慌了!
一、論如何反殺
相信大家面試都遇到過類似問題,因為源碼解析網(wǎng)上很多,很多人"考前突擊"一下。但是遇到喜歡問細(xì)節(jié)的面試官,終究難逃法眼,無處遁形。
遇到這個問題,我們?nèi)绾畏礆⒁徊ǎ肯旅嫖覀兙蛠砻枋鲆徊ㄕ鎸崍鼍啊.吘怪挥袚碛姓鎸崍鼍暗脑创a實戰(zhàn)(非常重要),遇到這類問題,才不至于出現(xiàn)猛虎落淚的情形。
二、真實場景描述
以這個聊天記錄中的真實場景為例:

>need-to-insert-img
那么我們把業(yè)務(wù)相關(guān)去掉,抽取一個最簡模型。我們在公司一般都會有自己的自定義異常,然后這個自定義異常一般放在common.jar給其他模塊依賴,比如我這里定義一個HelloException:
1public class HelloException extends RuntimeException { 2 3 publicHelloException() { 4 } 5 6 public HelloException(String message) { 7 super(message); 8 } 910}復(fù)制代碼
然后我們寫一個最簡單的Dubbo的demo,如下:
interface
1public interface DemoService {23 String sayHello(String name);45}復(fù)制代碼
provider
1public class DemoServiceImpl implements DemoService {23 public String sayHello(String name) {4 throw new HelloException("公眾號:肥朝");5 }67}復(fù)制代碼
consumer
1public class DemoAction { 2 3 private DemoService demoService; 4 5 public voidsetDemoService(DemoService demoService) { 6 this.demoService = demoService; 7 } 8 9 public void start() throws Exception {10 try {11 String hello = demoService.sayHello("公眾號:肥朝");12 } catch (HelloException helloException) {13 System.out.println("這里捕獲helloException異常");14 }15 }1617}復(fù)制代碼
按照聊天記錄的描述,此時consumer調(diào)用provider,provider拋出HelloException。但是consumer捕獲到的,卻不是HelloException。

那么我們運行看看:

果然如其所言。為什么會這樣呢?很多同學(xué)這種時候往往會采用最低效的解決辦法,把異常棧往微信群一丟,各種求助,但是往往毫無收獲,然后感嘆社會為何如此冷漠!
但是如果掌握了閱讀源碼的技能,就能夠直入源碼。出現(xiàn)異常我們首先看一下異常棧:

相信一眼望去,這行異常十分鮮明和出眾:

那么我們一探究竟:
1 public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { 2 try { 3 Result result = invoker.invoke(invocation); 4if(result.hasException() && GenericService.class != invoker.getInterface()) { 5 try { 6 Throwable exception = result.getException(); 7 8 // 如果是checked異常,直接拋出 9if(! (exception instanceof RuntimeException) && (exception instanceof Exception)) {10returnresult;11 }12 // 在方法簽名上有聲明,直接拋出13 try {14 Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());15 Class[] exceptionClassses = method.getExceptionTypes();16for(Class exceptionClass : exceptionClassses) {17if(exception.getClass().equals(exceptionClass)) {18returnresult;19 }20 }21 } catch (NoSuchMethodException e) {22returnresult;23 }2425 // 未在方法簽名上定義的異常,在服務(wù)器端打印ERROR日志26 logger.error("Got unchecked and undeclared exception which called by "+ RpcContext.getContext().getRemoteHost()27 +". service: "+ invoker.getInterface().getName() +", method: "+ invocation.getMethodName()28 +", exception: "+ exception.getClass().getName() +": "+ exception.getMessage(), exception);2930 // 異常類和接口類在同一jar包里,直接拋出31 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());32 String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());33if(serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){34returnresult;35 }36 // 是JDK自帶的異常,直接拋出37 String className = exception.getClass().getName();38if(className.startsWith("java.") || className.startsWith("javax.")) {39returnresult;40 }41 // 是Dubbo本身的異常,直接拋出42if(exception instanceof RpcException) {43returnresult;44 }4546 // 否則,包裝成RuntimeException拋給客戶端47returnnew RpcResult(new RuntimeException(StringUtils.toString(exception)));48 } catch (Throwable e) {49 logger.warn("Fail to ExceptionFilter when called by "+ RpcContext.getContext().getRemoteHost()50 +". service: "+ invoker.getInterface().getName() +", method: "+ invocation.getMethodName()51 +", exception: "+ e.getClass().getName() +": "+ e.getMessage(), e);52returnresult;53 }54 }55returnresult;56 } catch (RuntimeException e) {57 logger.error("Got unchecked and undeclared exception which called by "+ RpcContext.getContext().getRemoteHost()58 +". service: "+ invoker.getInterface().getName() +", method: "+ invocation.getMethodName()59 +", exception: "+ e.getClass().getName() +": "+ e.getMessage(), e);60 throw e;61 }62 }
手機(jī)上閱讀源碼或許并不友好,但是沒關(guān)系,上面都有完善的中文注釋。想表達(dá)的意思如下:
如果是checked異常,直接拋出。很明顯,我們的HelloException是RuntimeException,不符合。
在方法簽名上有聲明,直接拋出。很明顯,我們接口并未聲明該異常,不符合。
異常類和接口類在同一jar包里,直接拋出。很明顯,我們的異常類是在common.jar的,接口是在api.jar的,不符合。
是JDK自帶的異常,直接拋出。很明顯,這個HelloException是我們自定義的,不符合。
是Dubbo本身的異常(RpcException),直接拋出。很明顯,這個HelloException是我們自定義的,和RpcException幾乎沒有半毛錢關(guān)系。
否則,包裝成RuntimeException拋給客戶端。因為以上5點均不滿足,所以該異常會被包裝成RuntimeException異常拋出(重要)。
這也就是為什么我們catchHelloException是catch不到的,因為它包裝成RuntimeException了。
給自己的Java技術(shù)交流群打波廣告吧,想要學(xué)習(xí)Java架構(gòu)技術(shù)的朋友可以加我的群:710373545,群內(nèi)每晚都會有阿里技術(shù)大牛講解的最新Java架構(gòu)技術(shù)。并會錄制錄播視頻分享在群公告中,作為給廣大朋友的加群的福利——分布式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高并發(fā)、高可用架構(gòu))/微服務(wù)(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優(yōu)化(JVM、TomCat、MySQL)
三、Dubbo為什么這么設(shè)計?
也許你看到這里會覺得這個判斷好坑。Dubbo為什么要這么設(shè)計?我們看源碼,最重要的是知道作者為什么這么設(shè)計,只有知道為什么這么設(shè)計才是經(jīng)過了深度的思考,否則只能看后就忘。
其實Dubbo的這個考慮是基于序列化來考慮的。你想想,如果provider拋出一個僅在provider自定義的一個異常,那么該異常到達(dá)consumer,明顯是無法序列化的,所以你注意看Dubbo的判斷。
我們來看下它的判斷:
checked異常和RuntimeException是不同類型,強(qiáng)行包裝可能會出現(xiàn)類型轉(zhuǎn)換錯誤,因此不包,直接拋出。
方法簽名上有聲明,如果這個異常是provider.jar中定義的,因為consumer是依賴api.jar的,而不是依賴provider.jar,那么編譯都編譯不過。如果能編譯得過,說明consumer是能依賴到這個異常的,因此序列化不會有問題,直接拋出。
異常類和接口類在同一jar包里。provider和consumer都依賴API,如果異常在這個API,那序列化也不會有問題,直接拋出。
是JDK自帶的異常,直接拋出。provider和consumer都依賴jdk,序列化也不會有問題,直接拋出。
是Dubbo本身的異常(RpcException),直接拋出。provider和consumer都依賴Dubbo,序列化也不會有問題,直接拋出。
否則,包裝成RuntimeException拋給客戶端。此時,就有可能出現(xiàn)我說的那種,這個異常是provider.jar自定義的,那么provider拋出的時候進(jìn)行序列化,因為consumer沒有依賴provider.jar,所以異常到達(dá)consumer時根本無法反序列化。但是包裝成了RuntimeException異常則不同,此時異常就是JDK中的類了,到哪都能序列化。
既然都知道了原理了,那么很好解決。我隨便列舉一下,比如從規(guī)范上要求業(yè)務(wù)方接口聲明HelloException。