当前位置: 首页 > 后端技术 > Java

三重协议支持Java异常返回的设计与实现

时间:2023-04-01 20:56:21 Java

作者:ApacheDubbo贡献者陈景明背景在一些业务场景中,往往需要自定义异常来满足特定的业务。主流的用法是在catch中抛出异常,例如:publicvoiddeal(){try{//doSomething...}catch(IGreeterExceptione){...throwe;}}或者通过ExceptionBuilder返回相关异常对象给消费者:provider.send(newExceptionBuilders.IGreeterExceptionBuilder().setDescription('异常描述信息');抛出异常后,通过catching和instanceof判断具体的异常,然后做相应的业务处理,例如:try{greeterProxy.echo(REQUEST_MSG);}catch(IGreeterExceptione){//做相应的处理...}在Dubbo2.x版本中,可以使用上面的方法来捕获Provider端例外,随着云原生时代的到来,Dubbo也开启了3.0的里程碑,Dubbo3.0一个很重要的目标就是全面拥抱云原生,在3.0的众多特性中,一个很重要的变化就是支持新一代RPC协议Triple,Triple协议建立在HTTP2.0的基础上,对网关的穿透力强,兼容gRPC,提供RequestResponse、RequestStreaming、ResponseStreaming、Bi-定向流媒体;从Triple协议开始,Dubbo也支持基于IDL的服务定义。采用Triple协议的用户可以在provider端生成自定义的异常信息,记录异常产生的堆栈,triple协议可以保证用户在client端可以得到异常消息。Triple的返回异常会在AbstractInvoker的waitForResultIfSync中将异常信息栈统一封装成RpcException,所有来自Provider端的异常都会封装成RpcException类型抛出,避免用户根据特定的异常类型从Provider捕获异常,只能返回通过捕获RpcException获取信息,Provider携带的异常信息无法返回,只能获取打印的堆栈信息:try{greeterProxy.echo(REQUEST_MSG);}catch(RpcExceptione){e.printStackTrace();}自定义异常信息在社区也很流行,所以这次改动会支持自定义异常的功能,这样服务端就可以抛出自定义异常并被客户端捕获。Dubbo异常处理介绍下面我们从消费者的角度来看一个三重协议一元请求的大致流程:调用接口的方法并返回结果。Dubbo提供的代理工厂类是ProxyFactory,通过SPI机制默认实现的是JavassistProxyFactory。JavassistProxyFactory创建了一个继承自AbstractProxyInvoker类的匿名对象,并重写了抽象方法doInvoke。改写后的doInvoke只是将调用请求转发给Wrapper类的invokeMethod方法,并生成invokeMethod方法代码和其他一些方法代码。代码生成后,通过Javassist生成Class对象,最后通过反射创建Wrapper实例,然后通过InvokerInvocationHandler->InvocationUtil->AbstractInvoker->具体实现类向Provider发送请求。Provider进行相应的业务处理后,将相应的结果返回给Consumer端。Provider端的结果将被封装到AsyncResult中。在AbstractInvoker的具体实现类中,收到Provider的response后,会调用appResponse到recreate方法。如果appResponse中包含异常,则会抛给用户。大致流程如下:上述异常处理相关环节在Consumer端,在Provider端由org.apache.dubbo.rpc.filter.ExceptionFilter处理,是一系列职责中的一个环节chainFilter,专门用来处理异常。Provider端的Dubbo异常会被封装到appResponse中。下面的流程图揭示了ExceptionFilter源码的异常处理过程:当appResponse返回到Consumer端时,会调用InvocationUtil中AppResponse的recreate方法抛出异常,最终可以在Consumer端捕获:publicObjectrecreate()抛出Throwable{if(exception!=null){try{ObjectstackTrace=exception.getStackTrace();如果(stackTrace==null){exception.setStackTrace(newStackTraceElement[0]);}}catch(Exceptione){//ignore}throwexception;}returnresult;}三重通信原理上一节我们介绍了Consumer端Dubbo发送数据的大致流程,可以看到最后数据由AbstractInvoker的实现类发送。在Triple协议中,AbstractInvoker的具体实现类是TripleInvoker。TripleInvoker在发送前会启动监听器,监听Provider端的响应结果,并调用ClientCallToObserverAdapter的onNext方法发送消息,最后在底层封装成Netty请求发送数据。在发起正式请求之前,TripleServer会注册TripleHttp2FrameServerHandler,它继承自Netty的ChannelDuplexHandler,其作用是不断读取并解析channelRead方法中的Header和Data信息。来自消费者的信息流被反序列化并最终由ServerCallToObserverAdapter的调用方法处理。在invoke方法中,根据消费者请求的数据调用服务端对应的方法,异步等待结果;如果服务端抛出异常,调用onError方法处理,否则调用onReturn方法返回正常结果,代码逻辑大致如下:publicvoidinvoke(){...try{//调用invoke方法请求服务finalResultresponse=invoker.invoke(invocation);//异步等待结果response.whenCompleteWithContext((r,t)->{//如果异常不为空if(t!=null){//方法调用过程中出现异常,调用onError方法处理responseObserver.onError(t);return;}if(response.hasException()){//调用onReturn方法处理业务异常onReturn(response.getException());return;}...//正常返回结果onReturn(r.getValue());});}...}大致流程如下:实现版本理解了以上原理,我们就可以进行相应的改造。使消费者能够捕获异常的关键是在将异常对象和异常信息序列化后发送给消费者。常见的序列化协议有很多,比如Dubbo/HSF默认的hessian2序列化;广泛使用的JSON序列化;以及gRPC原生支持的protobuf(PB)序列化等。Triple协议默认使用Protobuf进行序列化,因为它兼容grpc。上述三种典型的序列化方案工作原理相似,但在实现和开发上略有不同。PB不能直接从序列化的字节流中生成内存对象,而Hessian和JSON都可以。后两者的反序列化过程并不依赖于“二方包”。序列化和反序列化代码与proto文件相同。只要客户端和服务端使用同一个proto文件进行通信,就可以构建通信的双方。可解析结构单个protobuf无法序列化异常信息,所以我们使用Wrapper+PB来序列化异常信息,抽象出一个TripleExceptionWrapperUtils来序列化异常,在trailer中使用TripleExceptionWrapperUtils来序列化异常。大致的代码流程如下:上面的实现方案看起来很合理,Provider端的异常对象和信息已经可以在Consumer端返回捕获了。但是仔细想想,还是有问题:通常在基于HTTP2的通信协议中,头部大小会有一定的限制。标头大小太大会导致严重的性能下降。为了保证性能,基于HTTP2的协议往往在建立连接时,需要协商最大的headersize,超过了就会发送失败。对于Triple协议,在设计之初是基于HTTP2.0的,可以无缝兼容Grpc,而Grpc的header只有8KB大小,异常对象的大小可能会超过限制,从而丢失异常信息;而多一个header携带序列化异常信息意味着用户可以添加的header数量会减少,占用其他header可以占用的空间。经过讨论,考虑将异常信息放在Body中,将序列化后的异常从trailer移到body中,使用TripleWrapper+protobuf进行序列化,将相关异常信息序列化后返回。社区围绕这个问题进行了一系列辩论。读者也可以先尝试思考一下:1、在body中携带返回的异常信息对应的HTTP头状态码是什么?2、基于http2构建的协议,按照主流的grpc实现方案,将相关错误信息放在trailer中。理论上是没有body的,上层协议也需要保持语义的一致性。如果此时在payload中返回异常对象,而grpc不支持在Body中返回序列化对象的功能,会不会破坏Http和grpc协议的语义?从这个角度来说,异常信息应该放在尾部。3.作为一个开源社区,不能一味的满足用户的需求。不规范的用法注定要被淘汰。我们应该尽量避免改变Protobuf的语义。在Wrapper层支持序列化异常是否可以满足需求?先回答第二个和第三个问题:HTTP协议没有规定当状态码不是2xx时不能返回body,返回后是否读取由用户决定。grpc使用protobuf进行序列化,所以不能返回异常;而trycatch机制是java特有的,其他语言没有相应的要求,但是grpc暂时不支持的功能一定不能不实现。Dubbo的设计目标之一就是希望与主流协议甚至架构看齐,但也希望能做一定程度的修改,满足用户的合理需求。而且从throw本身的语义出发,throw的数据不仅仅是错误信息,序列化后的异常信息是有业务属性的。从这个角度来说,不应该采用类似拖车的设计。至于单一的Wrapper层,是没办法和grpc通信的。至于Http头状态码设置为200,是因为返回的异常信息具有一定的业务属性,不再是简单的错误。这种设计也和grpc一致。以后可以新增三重状态进行网关采集。改版只需要在异常不为空时返回相关的异常信息,使用TripleWrapper+Protobuf将异常信息序列化,在消费端解析反序列化即可。从自定义异常的版本迭代可以看出,虽然只能添加一个小特性,但过程并不复杂,但由于互操作性、兼容性和协议的设计理念,思考和讨论可能比编写更多的时间对于代码。欢迎来到DubboStarhttps://github.com/apache/dubbo。搜索并关注官方微信公众号:ApacheDubbo,了解更多行业最新动态,掌握各大厂面试必备的Dubbo技能