封面:Dubbo的高级特性服务管控篇.png

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

上一篇,我们已经介绍了Dubbo在服务治理方面提供的特性,今天我们一起来看看Dubbo在其它方面提供的特性。同服务治理篇一样,本文的目的在于学会使用Dubbo在服务管控方面提供的特性,依旧不涉及任何实现原理。

工程结构

嗯~~
是这样的,因为电脑过于拉胯,而且IDEA着实有些吃内存了,所有我将测试工程按照子项目合并到一起了,目前我使用的工程结构是这样的:
图1:工程结构.png
子模块名由两部分组成:配置方式+功能,如:XMLProvider,表示以XML配置方式为主的服务提供方。
Tips:IDEA快要追上“内存雄狮”CLionl了。

本地存根(Stub)

使用Dubbo时,服务使用方只集成了接口,所有的实现全都在服务提供方,但部分场景中,我们希望服务使用方完成一些逻辑的处理,以此来减少RPC交互带来的性能消耗,例如:将参数校验放在服务使用方去做,减少一次与服务调用方的网络交互。
图2:本地存根的调用机制.jpeg
这种场景中,我们可以使用Dubbo提供的本地存根特性。我们有如下的服务提供方的工程结构:
图3:存根与接口声明.png
xml-provider-api模块中定义了对外提供服务的接口XMLProviderService,代码如下:

  1. public interface XMLProviderService {
  2. String say(String message);
  3. }

以及接口存根XMLProviderServiceStub,代码如下:

  1. public class XMLProviderServiceStub implements XMLProviderService {
  2. private final XMLProviderService xmlProviderService;
  3. public XMLProviderServiceStub(XMLProviderService xmlProviderService) {
  4. this.xmlProviderService = xmlProviderService;
  5. }
  6. @Override
  7. public String say(String message) {
  8. if (StringUtils.isBlank(message)) {
  9. return "message不能为空!";
  10. }
  11. try {
  12. return this.xmlProviderService.say(message);
  13. } catch (Exception e) {
  14. return "远程调用失败:" + e.getMessage();
  15. }
  16. }
  17. }

接着我们在服务使用方的工程中配置接口存根:

  1. <dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" stub="com.wyz.api.stub.XMLProviderServiceStub"/>

Tips:使用本地存根,要求存根的实现类必须有传入Proxy实例(服务使用方提生成的Proxy实例)的构造函数。

本地伪装(Mock)

本地伪装即我们在《Dubbo的高级特性:服务治理篇》中提到的服务降级,我们今天再稍微做一个补充。本地伪装是本地存根的一个子集,本地存根可以处理RPC调用环节中各种各样的错误和异常,而本地伪装则专注于处理RpcException(如网络失败,响应超时等)这种需要容错处理的异常
我们为XMLProviderService添加一个本地伪装服务XMLProviderServiceMock,工程结构如下:
图4:伪装与接口声明.png
XMLProviderServiceMock的代码如下:

  1. public class XMLProviderServiceMock implements XMLProviderService {
  2. @Override
  3. public String say(String message) {
  4. return "服务出错了!";
  5. }
  6. }

配置文件可以按如下方式配置:

  1. <dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" mock="true"/>

这种配置中,要求Mock的实现必须按照“接口名+Mock后缀”的方式进行命名;如果不想使用这种命名方式,可以使用全限名:

  1. <dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" mock="com.wyz.api.mock.XMLProviderServiceMock"/>

Tips:再“重复”一遍Mock的原因是,上一篇中出了一点错误,本应在<dubbo:reference>标签中做的配置,我写到了<dubbo:service>标签中,产生错误的原因还是没有动手在项目中写一写,哎,真应了那句“纸上得来终觉浅,绝知此事要躬行”。

参数回调

Dubbo支持参数回调功能,使服务提供方可以“反向”调用服务使用方,该功能是基于长链接生成的反向代理实现的,效果类似于异步调用。我们举个支付的例子:
XMLProvider工程的xml-provider-api模块中添加PaymentService接口,同时添加PaymentNotifyService用于通知PaymentService的结果:

  1. public interface PaymentService {
  2. void payment(String cardNo, PaymentNotifyService paymentNotifyService);
  3. }
  4. public interface PaymentNotifyService {
  5. void paymentNotify(String message);
  6. }

XMLProvider工程的xml-provider-service模块中实现PaymentService接口:

  1. public class PaymentServiceImpl implements PaymentService {
  2. @Override
  3. public void payment(String cardNo, PaymentNotifyService paymentNotifyService) {
  4. System.out.println("向卡号[" + cardNo + "]付钱!");
  5. // 业务逻辑
  6. paymentNotifyService.paymentNotify("付款成功");
  7. }
  8. }

执行PaymentService#payment方法,并调用PaymentNotifyService#paymentNotify方法通知服务调用方执行结果。
XMLConsumer工程中实现PaymentNotifyService接口:

  1. public class PaymentNotifyServiceImpl implements PaymentNotifyService {
  2. @Override
  3. public void paymentNotify(String message) {
  4. System.out.println("支付结果:" + message);
  5. }
  6. }

来看一下此时的工程结构:
图5:参数回调工程接口.png
接下来是XML的配置,参数回调中,我们需要关注的是服务提供方XMLProvider工程的xml-provider-service模块的配置:

  1. <bean id="paymentServiceImpl" class="com.wyz.service.impl.PaymentServiceImpl"/>
  2. <dubbo:service interface="com.wyz.api.PaymentService" ref="paymentServiceImpl" callbacks="10">
  3. <dubbo:method name="payment">
  4. <dubbo:argument index="1" callback="true"/>
  5. </dubbo:method>
  6. </dubbo:service>

配置通过第4行的<dubbo:argument index="1" callback="true"/>来确定PaymentService#payment方法中第2个(index从0开始)参数是回调参数;callbacks限制了同一个长链接下回调的次数,而不是总共回调的次数。
Tips:在实际的支付业务场景中,更倾向于异步处理,比如服务提供方在接收到支付请求是,启动新线程处理支付业务并调用通知接口,主线程返回成功接收支付请求。

异步调用

异步调用允许服务提供方立即返回响应,同时后台继续执行请求处理,当服务使用方请求响应结果时,服务提供方将结果返回。
图6:异步调用.jpeg
DUbbo支持两种异步调用方式:

  • 使用CompletableFuture接口
  • 使用RpcContext

DUbbo 2.7之后,DUbbo以CompletableFuture接口为异步编程的基础。

使用CompletableFuture实现异步调用

我们先来看如何使用CompletableFuture实现异步调用,声明CompletableFutureAsyncService接口:

  1. public interface CompletableFutureAsyncService {
  2. CompletableFuture<String> async(String message);
  3. }

接着是接口实现:

  1. public class CompletableFutureAsyncServiceImpl implements CompletableFutureAsyncService {
  2. @Override
  3. public CompletableFuture<String> async(String message) {
  4. return CompletableFuture.supplyAsync(() -> {
  5. System.out.println(Thread.currentThread().getName() + " say : " + message);
  6. try {
  7. TimeUnit.SECONDS.sleep(10);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. return "异步调用成功!";
  12. });
  13. }
  14. }

XML配置与普通的DubboRPC接口配置相同,xml-provider-service模块的配置:

  1. <bean id="completableFutureAsyncServiceImpl" class="com.wyz.service.impl.CompletableFutureAsyncServiceImpl" />
  2. <dubbo:service interface="com.wyz.api.CompletableFutureAsyncService" ref="completableFutureAsyncServiceImpl" />

XMLConsumer模块的配置:

  1. <dubbo:reference id="completableFutureAsyncService" interface="com.wyz.api.CompletableFutureAsyncService"/>

使用方式也非常简单:

  1. CompletableFuture<String> completableFuture = completableFutureAsyncService.async("Hello");
  2. System.out.println(completableFuture.get());

Tips

  • Dubbo中使用CompletableFuture与单独使用CompletableFuture并无什么差异~~
  • CompletableFutureAsyncServiceImpl的实现中打印接口名称的目的是为了清晰的展示出异步调用的效果;
  • CompletableFuture#supplyAsync(Supplier<U> supplier)默认使用ForkJoinPool#commonPool()
  • 重载方法CompletableFuture#supplyAsync(Supplier<U> supplier, Executor executor)允许使用自定义线程池。

    使用AsyncContext实现异步调用

    除了使用CompletableFuture外,还可以通过Dubbo定义的AsyncContext实现异步调用。先来编写接口和接口实现: ```java public interface RpcContextAsyncService { String async(String message); }

public class RpcContextAsyncServiceImpl implements RpcContextAsyncService {

  1. @Override
  2. public String async(String message) {
  3. final AsyncContext asyncContext = RpcContext.startAsync();
  4. new Thread(() -> {
  5. asyncContext.signalContextSwitch();
  6. asyncContext.write(Thread.currentThread().getName() + " say : " + message);
  7. }).start();
  8. // 异步调用中,这个返回值完全没有意义
  9. return null;
  10. }

}

  1. 服务提供方的配置与其它Dubbo接口的配置并无不同:
  2. ```xml
  3. <bean id="rpcContextAsyncServiceImpl" class="com.wyz.service.impl.RpcContextAsyncServiceImpl"/>
  4. <dubbo:service interface="com.wyz.api.RpcContextAsyncService" ref="rpcContextAsyncServiceImpl"/>

接着是服务使用方的配置,需要添加async参数:

  1. <dubbo:reference id="rpcContextAsyncService" interface="com.wyz.api.RpcContextAsyncService" async="true"/>

最后是在服务使用方中调用RPC接口:

  1. rpcContextAsyncService.async("Thanks");
  2. Future<String> future = RpcContext.getServiceContext().getFuture();
  3. System.out.println(future.get());

泛化调用

Dubbo的泛化调用提供了一种不依赖服务提供方API(SDK)的而调用服务的实现方式。主要场景在于网关平台的实现,通常网关的实现不应该依赖于其他服务的API(SDK)。
Dubbo官方提供了3种泛化调用的方式:

  • 通过API使用泛化调用
  • 通过Spring使用泛化调用(XML形式)
  • Protobuf对象泛化调用

这里我们介绍以XML的形式配置泛化调用的方式。

准备工作

首先我们再准备一个服务提供的工程GenericProvider,工程结构如下:
图7:GenericProvider工程.png
工程中定义了接口即实现类GenericProviderService和GenericProviderServiceImpl,代码如下:

  1. public interface GenericProviderService {
  2. String say(String message);
  3. }
  4. public class GenericProviderServiceImpl implements GenericProviderService {
  5. @Override
  6. public String say(String message) {
  7. return "GenericProvider say:" + message;
  8. }
  9. }

generic-dubbo-provider.xml中只需要正常配置GenericProvider提供的服务即可:

  1. '<bean id="genericProviderServiceImpl" class="com.wyz.service.impl.GenericProviderServiceImpl"/>
  2. <dubbo:service interface="com.wyz.service.api.GenericProviderService" ref="genericProviderServiceImpl" generic="true"/>

application.yml文件的配置我们就不多赘述了。

服务使用方的配置

回到XMLConsumer工程中,先配置Dubbo服务引用,xml-dubbo-consumer.xml中添加如下内容:

  1. <dubbo:reference id="genericProviderService" generic="true" interface="com.wyz.service.api.GenericProviderService"/>

参数generic声明这是一个泛化调用的服务。此时IDEA会将interface="com.wyz.service.api.GenericProviderService“标红,提示“Cannot resolve class ‘GenericProviderService’”,这个我们不需要关注,因为com.wyz.service.api包下确实不存在GenericProviderService接口。
接着我们来使用GenericProviderService接口:

  1. ApplicationContext context = SpringContextUtils.getApplicationContext();
  2. // genericProviderService是XML中定义的服务id
  3. GenericService genericService = (GenericService) context.getBean("genericProviderService");
  4. // $invoke的3个参数分别为:方法名,参数类型,参数
  5. Object result = genericService.$invoke("say", new String[]{"java.lang.String"}, new Object[]{"wyz"});
  6. System.out.println(result);

这样,我们就可以通过ApplicationContext获取到GenericProviderService接口提供的服务了。
Tips:SpringContextUtils用于获取ApplicationContext,代码如下:

  1. @Component
  2. public class SpringContextUtils implements ApplicationContextAware {
  3. private static ApplicationContext applicationContext = null;
  4. public static ApplicationContext getApplicationContext() {
  5. return SpringContextUtils.applicationContext;
  6. }
  7. @Override
  8. public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
  9. SpringContextUtils.applicationContext = applicationContext;
  10. }
  11. }

结语

好了,到目前为止,我们已经一起认识并学习了Dubbo中常用特性的配置与使用,当然了,经历了多年的发展,Dubbo的提供的特性远不止于此,如果想要了解更多内容,可以查看阿里巴巴提供的文档《Apache Dubbo微服务框架从入门到精通》。
下一篇,我们从服务注册部分正式开启对Dubbo实现原理的探索。


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!