1.2 本书Demo详解
1.2.1 Demo结构说明
首先讲解本书使用的Demo的结构,Demo使用Maven聚合功能构建,里面有三个模块,目录如图1.2所示。
图1.2
其中:
· Consumer模块为服务消费者相关模块,本书中所有与消费端有关的Demo都在该模块中,包含普通调用、各种异步调用、泛化调用、基于扩展接口实现的自定义负载均衡策略、集群容错策略等。
· Provider模块为服务提供者相关模块,本书中所有与服务提供端有关的Demo都在该模块中,包含服务接口的实现类、服务提供方的同步处理请求、各种异步处理请求的实现等。
· SDK模块是一个二方包,用来存放服务接口,这是为了代码复用,在服务提供者和消费者(泛化调用除外)的模块里需要引入这个二方包。
在Demo根目录下执行mvn clean install命令会将这些模块的Jar安装到本地仓库,要想在其他模块里引入这些模块,必须先执行这个安装步骤。
本书的Demo使用ZooKeeper(ZooKeeper 3.4.13)作为服务注册中心,大家可以在其官网下载,并做相应的配置后启动。另外,由于在编写本书时Dubbo最新的版本为2.7.1,所以本书基于Dubbo 2.7.1版本做讲解。
下面我们详细讲解Demo中的每个模块,这里假设ZooKeeper已经启动,并且地址为127.0.0.1:2181。
1.2.2 SDK模块
考虑到代码复用,SDK模块主要用来存放服务接口的定义与POJO类,下面我们看看其中的内容。
· GreetingService接口类主要用来演示同步调用,如下代码就定义了两个方法:
· GrettingServiceAsync接口类主要用来演示基于定义CompletableFuture签名的接口如何实现异步执行,如下代码就定义了一个返回值类型为CompletableFuture的方法:
· GrettingServiceRpcContext接口类主要用来演示AsyncContext如何实现异步执行,如下代码只有一个方法:
· PoJo类,一个简单的POJO(Plain Ordinary Java Object,简单Java对象)对象,用于演示泛化调用时的参数转换:
· Result类,POJO的返回值类型,用于演示泛化调用时的参数转换,定义如下:
1.2.3 同步发布与调用服务
首先我们看看服务提供端是如何使用Dubbo API发布服务的。在了解具体如何发布之前,先看看服务提供端针对SDK中com.books.dubbo.demo.api.GreetingService接口的实现类GreetingServiceImpl的代码:
上面的代码很简单,sayHello()方法休眠1s后返回拼接的字符串。其中RpcContext.getContext().getAttachment("company")获取调用方在上下文对象中附加的company变量的值,如果调用方在调用前没进行设置,则返回null。testGeneric()方法则把传入的poJo对象转换为字符串后返回。
下面我们看看ApiProvider类是如何发布服务的:
代码1创建了ServiceConfig实例,其泛型参数为GreetingService接口;代码2配置应用程序属性;代码3设置服务注册中心地址,可知服务注册中心使用了ZooKeeper,并且ZooKeeper的地址为127.0.0.1,启动端口为2181,服务提供方会将服务注册到该中心。
代码4将接口与实现类设置到ServiceConfig实例;代码5设置服务分组与版本,在Dubbo中,“服务接口+服务分组+服务版本”唯一地确定一个服务,同一个服务接口可以有不同的版本以便服务升级等,另外每个服务接口可以属于不同分组,所以当调用方消费服务时一定要设置正确的分组与版本。
代码7导出服务,启动NettyServer监听链接请求,并将服务注册到服务注册中心;代码8挂起线程,避免服务停止。
下面我们看看Consumer模块是如果同步调用服务的,APiConsumer类的代码如下:
代码9创建服务引用对象实例,其中泛型参数为GreetingService;代码10创建应用程序配置对象;代码11设置服务注册中心地址,可知服务注册中心使用了ZooKeeper,并且ZooKeeper的地址为127.0.0.1,启动端口为2181,服务消费端启动后会从该中心获取服务提供者地址列表。
代码12设置服务接口和超时时间;代码13设置自定义负载均衡策略与集群容错策略,后面会具体讲解;代码14设置服务分组与版本,需要注意的是分组与版本要与服务提供者的分组与版本一致;代码15引用服务;代码16设置隐式参数,然后服务提供者就可以在服务实现类方法里获取参数值;代码17同步发起远程调用,然后当前线程会被阻塞直到服务提供方把结果返回。
1.2.4 服务消费端异步调用服务
上一节我们讲解的服务消费端是同步调用的,也就是说调用线程在服务提供方结果返回前需要被阻塞,异步调用则是说消费端发起调用后会马上返回。本节我们将介绍两种异步调用方式。
1.Dubbo 2.6.*版本提供的异步调用
首先我们看看第一种异步调用方式,也就是Consumer模块中的APiAsyncConsumer类:
代码1创建引用实例,并设置属性;代码2设置调用为异步;代码3进行服务引用并且调用sayHello()方法,由于是异步调用,所以该方法马上返回null;代码4使用RpcContext.getContext().getFuture()获取future对象,然后在需要获取真实响应结果的地方调用future.get()来获取响应结果(调用future.get()会阻塞调用线程直到结果返回)。
上面介绍的基于从返回的future对象调用get()方法实现异步的缺点是当业务线程调用get()方法后业务线程会被阻塞,这不是我们想要的,所以Dubbo提供了在future对象上设置回调函数的方式,让我们实现真正的异步调用。下面是Consumer模块的APiAsyncConsumerForCallBack类:
上面的代码不同之处在于代码4,在((FutureAdapter)RpcContext.getContext().getFuture()).getFuture()获取的future对象上可以调用setCallback()方法设置一个回调函数,该回调函数有两个方法,当远端正常返回响应结果后,会回调done()方法,其参数response就是响应结果值;当发起远程调用发生错误时会回调caught()方法以打印错误信息。
设置回调的这种方式不会阻塞业务调用线程,这是借助了Netty的异步通信机制,Netty底层的I/O线程会在接收到响应后自动回调注册的回调函数,不需要业务线程干预。
2.Dubbo 2.7.*版本提供的异步调用
上面我们介绍了Dubbo 2.7.0版本前提供的异步调用方式,Future方式只支持阻塞式的get()接口获取结果。虽然通过获取内置的ResponseFuture接口可以设置回调,但获取ResponseFuture的API使用起来不方便,并且无法满足让多个Future协同工作的场景,功能比较单一。下面我们讲解Dubbo 2.7.0版本提供的基于CompletableFuture的异步调用。
下面是Consumer模块的APiAsyncConsumerForCompletableFuture2类:
代码4直接使用RpcContext.getContext().getCompletableFuture()获取CompletableFuture类型的future,然后就可以基于CompletableFuture的能力做一系列操作,这里通过调用whenComplete()方法设置了回调函数,作用是当服务提供端产生响应结果后调用设置的回调函数,函数内判断如果异常t不为null,则打印异常信息,否则打印响应结果。
1.2.5 服务提供端异步执行
1.基于定义CompletableFuture签名的接口实现异步执行
在Provider模块中,基于CompletableFuture签名的接口实现异步执行的接口实现类为GrettingServiceAsyncImpl,其代码如下:
通过上面的代码可知,基于定义CompletableFuture签名的接口实现异步执行需要接口方法的返回值为CompletableFuture,并且方法内部使用CompletableFuture.supplyAsync让本该由Dubbo内部线程池中的线程处理的服务,转换为由业务自定义线程池中的线程来处理,CompletableFuture.supplyAsync()方法会马上返回一个CompletableFuture对象(所以Dubbo内部线程池中的线程会得到及时释放),传递的业务函数则由业务线程池bizThreadpool执行。
需要注意的是,调用sayHello()方法的线程是Dubbo线程模型线程池中的线程,而业务处理是由bizThreadpool中的线程处理的,所以代码2.1保存了RPC上下文对象(ThreadLocal变量),以便在业务处理线程中使用。
然后,ApiProviderForAsync类用来发布服务,其代码如下:
上面的代码比较简单,这里就不再讲解了。服务发布后我们看看服务调用端如何调用。我们看看Consumer模块的APiConsumerForProviderAsync类:
上面代码的不同之处在于代码4,即调用greetingService.sayHello("world")直接返回了CompletableFuture对象,并在其上设置了回调函数。
2.使用AsyncContext实现异步执行
在Provider模块中,GrettingServiceAsyncContextImpl使用了AsyncContext实现异步执行,具体代码如下:
代码1创建了一个自定义线程池用来执行业务处理;代码2.1调用RpcContext.startAsync()方法开启服务异步执行,返回一个asyncContext,然后把服务处理任务提交到业务线程池后sayHello()方法就直接返回了null;异步任务内首先执行代码2.2切换任务的上下文,然后休眠500ms充当任务执行;最后,代码2.3把任务执行结果写入异步上下文。
这里由于具体执行业务处理的逻辑不在sayHello()方法所在的Dubbo内部线程池的线程里,所以Dubbo框架的线程不会被阻塞。
1.2.6 服务消费端泛化调用
前面我们讲到,当基于Dubbo API搭建Dubbo服务时,服务消费端需要引入一个SDK二方包,其中存放着服务提供端提供的所有接口类。
泛化接口调用方式主要在服务消费端没有API接口类及模型类元(比如入参和出参的POJO类)的情况下使用,其参数及返回值中没有对应的POJO类,所以所有POJO参数均转换为Map表示。使用泛化调用时,服务消费模块不再需要引入SDK二方包。
在Dubbo中,根据序列化方式的不同,分为三种泛化调用,分别为true、bean和nativejava。
1.generic=true方式
在Consumer模块的APiGenericConsumerForTrue类中演示了这种方式,其代码如下:代码1创建引用实例,这里需要注意的是,在泛型调用时,泛型参数固定为GenericService;代码2设置泛化调用类型为true;代码3获取引用,注意泛化值类型固定为GenericService。
代码4调用greetingService.$invoke()方法,其中第一个参数为sayHello,说明要调用sayHello()方法,第二个参数为sayHello的参数类型,第三个参数为sayHello参数的值。
代码5和代码6是对方法testGeneric()发起泛型调用,这里的第二个参数为testGeneric入参的POJO类型;第三个参数为入参的值,这里由于为POJO类,所以把POJO类的属性转换到了Map里,并且Map中固定有一个key为class的属性,其对应的value为第二个参数的包名加类名,这是为了在服务提供端进行反射使用。
另外,由于这里的testGeneric()方法返回值是Result类型,是POJO类,所以返回值也会被转换为Map。
2.generic=bean方式
在Consumer模块的APiGenericConsumerForBean类中演示了这种方式,其代码如下:
上面代码的不同之处在于代码2设置了泛型类型为bean,这意味着对参数使用JavaBean方式进行序列化,如代码4使用JavaBeanSerializeUtil.serialize把参数进行序列化,然后把序列化结果作为第三个参数,最后执行泛化调用。
代码5对响应结果使用JavaBeanSerializeUtil.deserialize进行反序列化就可以得到我们可以理解的响应结果(之所以进行反序列化,是因为服务提供方会对返回结果进行序列化)。
3.generic=nativejava方式
在Consumer模块的APiGenericConsumerForNativeJava类中演示了这种方式,其代码如下:
上面代码的不同之处在于代码2设置了泛型类型nativejava,这意味着对参数使用nativejava方式进行序列化,如代码4使用nativejava对把参数进行序列化,然后把序列化结果作为第三个参数,最后执行泛化调用。
代码5对响应结果使用nativejava进行反序列化就可以得到我们可以理解的响应结果(之所以进行反序列化,是因为服务提供方会对返回结果进行序列化)。
1.2.7 服务消费端本地服务mock与服务降级
1.本地服务mock
服务消费端本地服务mock主要用来做本地测试用,当服务提供端服务不可用时,使用本地mock服务可以模拟服务提供端来让服务消费方测试自己的功能,而不需要发起远程调用,在Demo的Consumer模块中实现了mock功能。
要实现mock功能,首先需要消费端先实现服务接口的mock实现类,在Demo中我们是对接口com.books.dubbo.demo.api.GreetingService进行模拟(mock)的,其mock实现类为com.books.dubbo.demo.api.GreetingServiceMock。需要注意的是,mock实现类必须要符合“接口包名.类名Mock”格式,否则启动时候会抛出“throw new IllegalStateException("No default constructor from mock class"+mockClass.getName(),e)”异常,这在后面的高级篇会做讲解。
Demo中的GreetingServiceMock类的代码如下:
上面代码中的mock实现类比较简单,对于sayHello()方法来说,mock返回mock value;对于testGeneric()方法来说,mock返回null。
创建完mock类后,我们看看在APiConsumerMock类中是如何开启mock功能的:
在上面的代码中,代码5设置启动时不检查服务提供端是否可用,然后设置mock为true开启mock功能,接着执行该类,并且保证GreetingServiceMock类的class文件位于classpath下,运行后会输出mock value字符串,说明mock功能生效了。
需要注意的是,在执行mock服务实现类mock()方法前,会先发起远程调用,当远程调用失败(比如服务不存在)时,才会降级执行mock功能。
2.服务降级
Dubbo提供了一些服务降级措施,当服务提供端某一个非关键的服务出错时,可以手动对消费端的调用进行降级,这样服务消费端就避免了再去调用出错的服务,以避免加重服务提供端的负担。
下面我们看看Dubbo提供的两种服务降级策略如何使用。
· force:return策略:当服务调用方设置某个接口的降级策略为这种方式时,服务调用方在调用该接口服务时会直接在客户端内返回设置的mock值,而不会在通过远程调用方式调用服务提供者,比如配置URL为override://0.0.0.0/com.books.dubbo.demo.api.GreetingService?category=configurators&dynamic=false&application=first-dubbo-consumer&"+"mock="+type+":return+null&group=dubbo&version=1.0.0",其中mock=force:return+null表示服务调用方在调用该服务的方法时都直接返回mock的null值,而不发起远程调用。需要注意的是,在URL里要指明是对哪个接口的哪个分组的哪个版本的服务进行降级,另外category必须为configurators,application为你的服务调用方的应用名称,也就是ApplicationConfig的name值。override://0.0.0.0/标识该降级策略对所有的服务消费者生效。该URL会被保存到ZooKeeper中,持久化存放该设置,当消费端启动时会获取到该配置。
· fail:return策略:表示服务调用方调用服务提供方的服务失败后再返回mock值,与force:return的区别是前者如果调用服务提供者成功,则返回正常的结果,如果调用失败则返回mock的值。这个功能和上一节讲解的本地服务mock功能一致。
在Demo的Consumer的APiConsumerMockResult类中演示了如何对一个服务进行降级设置:
代码1通过增强SPI获取服务注册中心的工厂,这里获取的为ZooKeeper工厂,然后代码2获取ZooKeeper客户端,代码3将服务降级方案注册到ZooKeeper。其中参数type为降级方案(比如force或者fail),执行代码后,配置信息会被保存到服务注册中心,然后当消费端启动时会获取到该配置,并发返回null结果。当服务提供方的服务恢复后,可以执行上面代码4以取消服务降级方案。
1.2.8 隐式参数传递
在Consumer模块的APiConsumer类中使用隐式参数设置:
上面的代码3在消费端发起远程调用前,通过RpcContext.getContext().setAttachment设置了key为company、value为alibaba的键值对,这个键值对会随着远程调用通过网络传递给服务提供端。下面我们看看Demo的Provider模块的GreetingServiceImpl类:
上面的代码在服务提供端服务实现类中通过RpcContext.getContext().getAttachment("company")可以获取消费端传递的变量值。
1.2.9 本地服务暴露与引用
远程服务暴露与引用,是指提供方与消费方通过网络来进行通信,具体通信流程如图1.3所示。
图1.3
其实Dubbo还提供了一种本地服务暴露与引用的方式,这在同一个JVM进程中同时发布与调用同一个服务时显得比较重要,因为如果当前JVM内要调用的服务在本JVM进程内有提供,则避免了一次远程过程调用,而是直接在JVM内进行通信,如图1.4所示。
图1.4
在Demo的Provider模块的APiConsumerInJvm类中演示了本地服务暴露与引用的实例:
在上面的代码中,exportService()方法导出服务(包含本地服务与远程服务),referService()方法进行服务引用,由于在JVM内存在要引用的服务,所以实际是进行了本地调用,大家可以在InjvmInvoker的invoke()方法内添加断点以便验证是否真的调用了本地服务。