原文地址 mp.weixin.qq.com
根据我的面试经验而言, 能在简历上写上原理
、源码
等关键词的, 是非常具备核心竞争力的. 上周和一个公众号粉丝交流面试情况如下
面试的时候, 把源码一波分析, 令面试官虎躯一震
! 在一阵前戏过后, 以为接下来无非就是身体的一顿抽搐一切变得索然无味, 不料面试官来了句令剧情发生了反转
“你对 Dubbo 源码这么熟悉, 那请问你使用的时候, 有没有遇到什么坑”
我擦, 毫无准备的他,菊花顿时一紧!
此时就面临唬住了50K,唬不住就只能15K
的局面,我开始慌了!
论如何反杀
相信大家面试都遇到过类似问题, 因为源码解析网上很多, 很多人 “考前突击” 一下, 但是遇到喜欢问细节的面试官, 终究难逃法眼, 无处遁形. 遇到这个问题, 我们如何反杀一波? 那么我就从一次聊天记录说起, 拥有**真实
**场景的源码实战 (非常重要), 遇到这类问题, 才不至于出现猛虎落泪
的情形:
真实场景描述
那么我们把业务相关去掉, 抽取一个最简模型. 我们在公司, 一般都会有自己的自定义异常, 然后这个自定义异常一般放在common.jar
给其他模块依赖, 比如我这里定义一个HelloException
1public class HelloException extends RuntimeException {
2
3 public HelloException() {
4 }
5
6 public HelloException(String message) {
7 super(message);
8 }
9
10}
然后我们写一个最简单的 Dubbo 的 demo, 如下
interface:
1public interface DemoService {
2
3 String sayHello(String name);
4
5}
provider:
1public class DemoServiceImpl implements DemoService {
2
3 public String sayHello(String name) {
4 throw new HelloException("公众号:肥朝");
5 }
6
7}
consumer:
1public class DemoAction {
2
3 private DemoService demoService;
4
5 public void setDemoService(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 }
16
17}
按照聊天记录的描述,此时 consumer 调用 provider,provider 抛出HelloException
. 但是 consumer 捕获到的,却不是HelloException
.
那么我们运行看看:
果然如该同事所言. 为什么会这样呢? 不要慌,九浅一深直入源码。出现异常我们首先看一下异常栈:
除非撸多了看不清,否则这行异常能很直接的定位到问题所在:
1com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:108)
接下来让我们一探究竟:
1 public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
2 try {
3 Result result = invoker.invoke(invocation);
4 if (result.hasException() && GenericService.class != invoker.getInterface()) {
5 try {
6 Throwable exception = result.getException();
7
8 // 如果是checked异常,直接抛出
9 if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {
10 return result;
11 }
12 // 在方法签名上有声明,直接抛出
13 try {
14 Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
15 Class<?>[] exceptionClassses = method.getExceptionTypes();
16 for (Class<?> exceptionClass : exceptionClassses) {
17 if (exception.getClass().equals(exceptionClass)) {
18 return result;
19 }
20 }
21 } catch (NoSuchMethodException e) {
22 return result;
23 }
24
25 // 未在方法签名上定义的异常,在服务器端打印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);
29
30 // 异常类和接口类在同一jar包里,直接抛出
31 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
32 String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
33 if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
34 return result;
35 }
36 // 是JDK自带的异常,直接抛出
37 String className = exception.getClass().getName();
38 if (className.startsWith("java.") || className.startsWith("javax.")) {
39 return result;
40 }
41 // 是Dubbo本身的异常,直接抛出
42 if (exception instanceof RpcException) {
43 return result;
44 }
45
46 // 否则,包装成RuntimeException抛给客户端
47 return new 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);
52 return result;
53 }
54 }
55 return result;
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 }
手机上阅读源码或许并不友好, 但是没关系, 上面都有完善的中文注释, 他想表达的意思如下:
如果是 checked 异常,直接抛出。很明显,我们的
HelloException
是RuntimeException
, 不符合;在方法签名上有声明,直接抛出。很明显, 我们接口并未声明该异常, 不符合;
异常类和接口类在同一 jar 包里,直接抛出。很明显,我们的异常类是在 common.jar 的, 接口是在 api.jar 的,不符合;
是 JDK 自带的异常,直接抛出。很明显, 这个
HelloException
是我们自定义的,不符合;是 Dubbo 本身的异常 (RpcException),直接抛出。很明显, 这个
HelloException
是我们自定义的,和RpcException
几乎没有半毛钱关系,不符合;否则,包装成 RuntimeException 抛给客户端. 因为以上 5 点均不满足, 所以该异常会被包装成
RuntimeException
异常抛出 (重要);
这也就是为什么我们 catchHelloException
是 catch 不到的, 因为他包装成RuntimeException
了
Dubbo 为什么这么设计
也许你看到这里会觉得这个判断好坑. Dubbo 为什么要这么设计? 我们看源码, 最重要的是知道作者为什么这么设计, 只有知道**为什么这么设计
**才是经过了深度的思考, 否则看时高潮, 看后就忘。
其实 Dubbo 的这个考虑,是基于序列化来考虑的。你想想, 如果 provider 抛出一个仅在 provider 自定义的一个异常, 那么该异常到达 consumer, 明显是无法序列化的。所以你注意看 Dubbo 的判断. 我们来看下他的判断:
1.checked 异常和 RuntimeException 是不同类型, 强行包装可能会出现类型转换错误, 因此不包, 直接抛出
方法签名上有声明. 方法签名上有声明, 如果这个异常是 provider.jar 中定义的, 因为 consumer 是依赖 api.jar 的, 而不是依赖 provider.jar. 那么编译都编译不过, 如果能编译得过, 说明 consumer 是能依赖到这个异常的, 因此序列化不会有问题, 直接抛出
异常类和接口类在同一 jar 包里. provider 和 consumer 都依赖 api, 如果异常在这个 api, 那序列化也不会有问题, 直接抛出
是 JDK 自带的异常,直接抛出. provider 和 consumer 都依赖 jdk, 序列化也不会有问题, 直接抛出
是 Dubbo 本身的异常 (RpcException),直接抛出. provider 和 consumer 都依赖 Dubbo, 序列化也不会有问题, 直接抛出
否则,包装成 RuntimeException 抛给客户端. 此时, 就有可能出现我说的那种, 这个异常是 provider.jar 自定义的, 那么 provider 抛出的时候进行序列化, 因为 consumer 没有依赖 provider.jar, 所以异常到达 consumer 时, 根本无法反序列化. 但是包装成了
RuntimeException
异常则不同, 此时异常就是 JDK 中的类了, 到哪都能序列化.
如何解决
既然都知道了原理了, 那么问题就很好解决了。比如从规范上要求业务方接口声明HelloException。
GET 到了么?