开篇
本文主要介绍一些dubbo的高级应用。
在平时使用dubbo时,最常用的还是直接加@DubboService
、@DubboReference
注解,dubbo还提供了更多的高级功能供我们使用。
本文采用的示例是在dubbo系列第一篇Dubbo基础应用的基础上进行修改的,这里不再赘述。
服务降级
服务降级,是针对于某个服务提供者而言的,服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。
比如消费者在调用提供者时,服务超时而导致调用失败了,就可以采用服务降级来进行容错。
例如,现在服务端配置超时为2s,消费端配置超时为1s,加上容错措施。
@Service
public class ConsumerService {
@DubboReference(timeout = 1000, retries = 3, mock = "fail: return Error")
private HelloService helloService;
public void sayHello() {
String result = helloService.sayHello("world");
System.out.println(result);
}
}
复制代码
执行后,经过3次重试后,会直接返回 Error,而不是抛出异常。
mock=fail: return Error
表示,在消费者调用提供者失败后,返回Error。
mock=force: return Error
表示,不会执行调用,直接返回Error。
还可以创建一个Mock类来实现。
在dubbo-api
项目创建一个类,实现HelloService
接口来做Mock。
public class HelloServiceMock implements HelloService {
public String sayHello(String name) {
return "Mock " + name;
}
}
复制代码
消费端调用
@EnableDubbo
@SpringBootApplication
public class ConsumerApplication {
@DubboReference(timeout = 1000, retries = 3, mock = "true")
private HelloService helloService;
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
ConsumerApplication consumerApplication = context.getBean(ConsumerApplication.class);
consumerApplication.sayHello("world");
}
public void sayHello(String name) {
String result = helloService.sayHello(name);
System.out.println(result);
}
}
复制代码
本地伪装
本地伪装和服务降级的功能是一样的,都是通过mock
来实现,具体用法可以参考服务降级。
本地存根
本地存根是一段代码逻辑,这段代码逻辑一般是服务端提供者提供,但是在服务消费端执行。服务提供者可以利用这种机制在服务消费者远程调用服务提供者之前或之后再做一些其他事情,比如结果缓存、请求参数验证或其他逻辑。
现在我们来创建一个本地存根类HelloServiceStub
,同样需要实现HelloService
接口。本地存根类需要持有HelloService
一个对象。当消费者调用时会先执行本地存根这个类。
public class HelloServiceStub implements HelloService {
private final HelloService helloService;
public HelloServiceStub(HelloService helloService) {
this.helloService = helloService;
}
public String sayHello(String name) {
System.out.println("before execute remote service, parameter: " + name);
String result = this.helloService.sayHello(name);
System.out.println("after execute remote service, result: " + result);
return "Stub: " + name;
}
}
复制代码
消费者执行代码
@EnableDubbo
@SpringBootApplication
public class ConsumerApplication {
@DubboReference(stub = "true")
private HelloService helloService;
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
ConsumerApplication consumerApplication = context.getBean(ConsumerApplication.class);
consumerApplication.sayHello("world");
}
public void sayHello(String name) {
String result = helloService.sayHello(name);
System.out.println(result);
}
}
复制代码
查看执行结果
代码中的stub="true"
,代表启用本地存根,dubbo会根据调用的接口全限定名+"Stub"
来充当本地存根类,当然也支持stub="某个类的全限定名"
,如果找不到这个类,程序就会报错。
当消费者调用sayHello
方法时,是先执行的stub
里面的sayHello
方法,在stub
类里面执行一段逻辑后,才去真正执行sayHello
方法,这也相当于mock的另一种实现方式。
参数回调
参数回调是指,服务消费端在调用服务提供者的时候,调用完成后给服务消费端一个回调(再调用服务消费端一个方法)。
一般都是消费端来调用服务端,如果在调用服务端后,想让服务端再调用对应的消费端就需要用到这种机制了。
dubbo-api`里增加回调接口`HelloServiceListener
public interface HelloServiceListener {
void changed(String msg);
}
复制代码
dubbo-provider
服务提供者增加回调配置
@DubboService(methods = {@Method(name = "sayHello", arguments = {@Argument(index = 1, callback = true)})})
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name, HelloServiceListener listener) {
System.out.println("provider received invoke of sayHello: " + name);
listener.changed("Callback " + name);
return "Hello, " + name;
}
}
复制代码
dubbo-consumer
服务消费者进行调用
@EnableDubbo
@SpringBootApplication
public class ConsumerApplication {
@DubboReference
private HelloService helloService;
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
ConsumerApplication consumerApplication = context.getBean(ConsumerApplication.class);
consumerApplication.sayHello("world");
}
public void sayHello(String name) {
String result = helloService.sayHello(name, new HelloServiceListenerImpl());
System.out.println(result);
}
}
public class HelloServiceListenerImpl implements HelloServiceListener {
@Override
public void changed(String msg) {
System.out.println("Changed :" + msg);
}
}
复制代码
如果有多个回调的话,可以再加一个参数来区分。
异步调用
Dubbo支持多种异步调用,可以通过注解或xml将一个方法标记为异步方法,也可以将一个类的方法的返回值类型设置为CompletableFuture
。
注解形式
@DubboReference(methods = @Method(name = "sayHello", async = true))
private HelloService helloService;
复制代码
通过这种方式,直接调用方法会得到一个null
,而不是结果。需要通过RpcContext
来获取返回值。
// 会返回null
String result = helloService.sayHello(name);
System.out.println(result);
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
helloFuture.whenComplete((v, t) -> {
if (t == null) {
System.out.println("result: " + v);
} else {
t.printStackTrace();
}
});
复制代码
或者通过asyncCall
来完成异步调用。
CompletableFuture<String> f = RpcContext.getContext().asyncCall(() ->
helloService.sayHello("async"));
System.out.println("result: " + f.get());
复制代码
CompletableFuture
将方法声明为CompletableFuture
类型
CompletableFuture<String> sayHello(String name);
复制代码
这样消费端就不用使用RpcContext
就可以完成异步调用。
CompletableFuture<String> helloFuture = helloService.sayHello(name);
helloFuture.whenComplete((v, t) -> {
if (t == null) {
System.out.println("result: " + v);
} else {
t.printStackTrace();
}
});
复制代码
泛化调用
Dubbo提供了一个类GenericService
,他可以调用所有的服务,通常用来做测试。使用这个类的一个好处是,我们可以不用引入额外的jar就可以调用服务,不过书写起来不叫麻烦。需要指定generic
为true
。
@EnableDubbo
@SpringBootApplication
public class GenericApplication {
@DubboReference(id = "genericService", interfaceName = "cn.juejin.dubbo.api.HelloService", generic = true)
private GenericService genericService;
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(GenericApplication.class, args);
GenericService genericService = (GenericService) context.getBean("genericService");
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"generic"});
System.out.println(result);
}
}
复制代码
泛化服务
在服务端实现了GenericService
接口的类,提供的服务称为泛化服务,是用来实现一个通用的服务测试框架。
服务端定义一个泛化服务
@DubboService(interfaceName = "cn.juejin.dubbo.api.HelloService")
public static class GenericServiceImpl implements GenericService {
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
System.out.println("执行泛化服务: " + method);
return "Hello, " + args[0];
}
}
复制代码
在消费端调用时,依然可以按之前的方式调用。
@DubboReference
private HelloService helloService;
JAVA SPI
SPI
是英文Service Provider Interface
的缩写,是一种服务发现机制。SPI
是一种可插拔机制,需要定义一个接口,然后针对不同的场景进行实现,体现了我们常说的面向接口编程思想。
JAVA
自带的SPI
机制是JDK1.6
引入的,下面来看一个示例,了解下SPI
的使用方式:
首先定义一个接口
public interface Mascot {
String getName();
}
复制代码
针对这个接口创建两个实现类
public class YoYo implements Mascot{
@Override
public String getName() {
return "YoYo";
}
}
public class Click implements Mascot {
@Override
public String getName() {
return "Click";
}
}
复制代码
在resources
目录创建文件夹META-INF/services
,然后创建一个名字问接口名的文件cn.juejin.spi.Mascot
,内容为两个类的全名。
cn.juejin.spi.Click
cn.juejin.spi.YoYo
复制代码
好了,创建一个启动类来启动测试下吧
public class Main {
public static void main(String[] args) {
ServiceLoader<Mascot> mascots = ServiceLoader.load(Mascot.class);
for (Mascot mascot : mascots) {
System.out.println(mascot.getName());
}
}
}
复制代码
右键运行,结果如下:
JAVA SPI
是通过ServiceLoader
来加载接口对应的实现类,但是他会将文件里的所有类都进行加载进来实例化,并不能按需加载。
可能我们并没有这样使用过JDK的SPI
,但是我们间接的使用过,比如JDK中大名鼎鼎的java.sql.Driver
接口,当我们引入MySQL
的jar包时,JDK就会加载MySQL
的实现类,当我们引入Oracle
的jar包时,他就会加载Oracle
的实现类。这就是通过SPI类实现的。
Dubbo SPI
JAVA SPI
有个很明确的缺点是,不能按需加载,接口对应的文件里有多少个类,就会实例化多少。通常情况下,我们更希望的是加载一个指定的类。在配置文件里,为每一个类定义一个key,需要加载的时候,指定这个key就好了。
click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo
复制代码
JAVA SPI
不能满足这样的功能,所以Dubbo自己实现类一套SPI机制,即Dubbo SPI
。
我们都知道Dubbo支持多种协议:dubbo
、http
、redis
、rmi
、rest
、thrift
等。我们需要哪些协议,只需要在配置文件里指定就好了,这就是通过Dubbo SPI
来实现的。
下面我们来看下一段获取http协议的代码:
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getExtension("dubbo");
System.out.println(protocol);
复制代码
其中Protocol
是一个协议接口,dubbo支持的协议都实现了这个接口。ExtensionLoader
是一个扩展点加载器,上面例子第一行是说,获取一个Protocol
的扩展点的扩展点加载器,dubbo
、http
、redis
这些协议就是Protocol
的扩展点。第二行是说,要获取dubbo
这个具体的扩展点。运行之后会获取dubbo
对应的一个扩展点实现类。
好了,这是Dubbo SPI
的一个简单用法,知道了怎么用之后,那我们把文中第一个示例改造一下,使用Dubbo SPI
来实现吧。
首先,在接口上加上@SPI
注解。
@SPI
public interface Mascot {
String getName();
}
复制代码
在resources
目录创建文件夹META-INF/dubbo
,然后创建一个名字问接口名的文件cn.juejin.spi.Mascot
,内容如下。
click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo
复制代码
改造一下Main函数,并运行看一下吧。
我们指定了,要获取yoyo
这个扩展点,然后输出了YoYo
这个类的实例化对象。
SPI AOP
Dubbo SPI
只是做了这些功能吗?可以指定一个key进行加载?当然不是啦,Dubbo SPI
还支持了类似AOP
的功能,可以实现在一个类中注入另一个对象。
我们为Mascot
接口创建一个Wrapper
类
public class MascotWrapper implements Mascot {
private final Mascot mascot;
public MascotWrapper(Mascot mascot) {
this.mascot = mascot;
}
@Override
public String getName() {
System.out.println("MascotWrapper.....");
return mascot.getName();
}
}
复制代码
修改META-INF/dubbo/cn.juejin.spi.Mascot
文件内容
click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo
cn.juejin.spi.MascotWrapper
复制代码
现在我们再次运行刚才的Main函数,会输出什么呢?还是YoYo吗?来看一下:
我们可以看到,虽然我们获取的是yoyo
,但实际上输出的是MascotWrapper
,但MascotWrapper
里面的mascot
属性值却是yoyo
,这就是Dubbo SPI
提供的类似AOP的功能,或者叫做依赖注入。当然,如果你愿意,也可以创建多个Wrapper
,比如MascotWrapperWrapper
、MascotWrapperWrapperWrapper
。。。
另外,这个命名也不一定飞叫Wrapper
,名字是可以随便起的,你也可以叫MascotRap
,dubbo是根据这个类的构造器来确定的。
源码分析
ExtensionLoader<Mascot> extensionLoader
= ExtensionLoader.getExtensionLoader(Mascot.class);
复制代码
这一行代码比较简单,获取Mascot接口对应的扩展点加载器。
getExtensionLoader
方法,首先是一堆判断,可以先忽略,先从map
缓存里获取接口类对应的扩展点加载器,如果缓存里没有,那就创建一个,然后放到缓存里,再返回。
Mascot yoyo = extensionLoader.getExtension("yoyo");
复制代码
这一行代码就是要获取一个具体的扩展点了,看下具体的实现。这段代码也比较简单,先判断是否为默认扩展点,如果是返回默认的扩展点,否则,判断缓存中是否已经创建了扩展点,如果没有创建过则创建对应的扩展点并放入缓存中,然后返回。
如果name
的值是true
,将得到一个默认的扩展点,那什么是默认的扩展点呢?我们来试一下。
首先对接口进行改造:
@SPI("yoyo")
public interface Mascot {
String getName();
}
复制代码
把那name
值改为true
,运行一下,我们可以看到依然获取到了YoYo这个默认的对象。
继续看源码,getOrCreateHolder
获取class对应的holder对象,这个对象作为一个锁,跟并发有关系,防止重复生成。
createExtension
方法是创建扩展点。
getExtensionClasses
方法是获取解析配置文件,查找所有的扩展点并缓存起来。查找配置文件的路径为META-INF/dubbo/internal/
、META-INF/dubbo/
、META-INF/services/
。查找到之后,把对应的key
和类class
文件放到map
中。再根据传进来的name
获取对应的class
文件,并进行实例化。
injectExtension(instance);
复制代码
对实例化后的对象,看里面有哪些属性并进行赋值。
这里就是我们刚才写的Wrapper
类,这里遍历拿到的所有包装类,根据传过来的type寻找对应的构造器,并进行实例化。实例化后再对包装类的属性进行注入。
这里就是对Dubbo SPI
获取扩展点的简单源码解析。
接下来,我们来聊一聊,Dubbo是如何解析META-INF
文件夹下接口文件的。
源码分析
加载扩展点类
解析文件的方法入口是在这里:org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
。
private Map<String, Class<?>> loadExtensionClasses() {
// 缓存默认扩展点
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) {
// 加载文件夹中接口类的内容
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
strategy.overridden(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
复制代码
这段代码主要干了两件事:
缓存默认扩展点,也就是
@SPI
的值作为默认扩展点。根据某种策略来读取对应的文件夹。
读取文件里的文件找到类,访问
map
中,并且返回。
默认扩展点
缓存默认扩展点
private void cacheDefaultExtensionName() {
// @SPI("xxx")
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
复制代码
先判断接口上有没有@SPI
注解,如果没有的话也就没有默认的扩展点。
如果有@SPI
注解,并且有对应的value
值,则把value
值作为默认扩展点的名字。
读取策略
从上面代码我们可以看到,有个for循环,遍历某种策略,来读取文件内容,我们来看下这种策略。
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
private static LoadingStrategy[] loadLoadingStrategies() {
return StreamSupport.stream(ServiceLoader.load(LoadingStrategy.class)
.spliterator(), false)
.sorted()
.toArray(LoadingStrategy[]::new);
}
复制代码
从这段代码中,可以看到一个熟悉的类ServiceLoader
,这个不就是JAVA SPI
的类吗?那肯定在某个META-INF/services/
目录下有个文件叫org.apache.dubbo.common.extension.LoadingStrategy
的文件。根据dubbo的命名规则,应该在common工程里。
果然是,来看下里面的内容:
org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.apache.dubbo.common.extension.ServicesLoadingStrategy
复制代码
也就是这3种策略来读取文件。打开3个类的内容,这3个类分别对应3个文件夹,内容比较简单。
DubboInternalLoadingStrategy
类对应META-INF/dubbo/internal/
。
DubboLoadingStrategy
类对应META-INF/dubbo/
。
ServicesLoadingStrategy
类对应META-INF/services/
。
loadDirectory
读取目录文件内容,我们看到调了两次loadDirectory
,其中第二个把org.apache
替换为com.alibaba
,这个是为了做兼容,因为dubbo原来是alibaba
开发的,后来捐献给apache
。
从代码中看到,先把上面3个文件夹拼接上接口类的名字作为文件名,然后通过类加载器来找到文件,再通过loadResource
方法解析文件。
值的一说的是,类加载器是用的哪个类加载器呢?
private static ClassLoader findClassLoader() {
return ClassUtils.getClassLoader(ExtensionLoader.class);
}
public static ClassLoader getClassLoader(Class<?> clazz) {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
} catch (Throwable ex) {
}
if (cl == null) {
cl = clazz.getClassLoader();
if (cl == null) {
try {
cl = ClassLoader.getSystemClassLoader();
} catch (Throwable ex) {
}
}
}
return cl;
}
复制代码
首页查看当前线程的类加载器,如果没有的话,就找ExtensionLoader
这个类是由哪个类加载器进行加载的,如果还没有的话,就返回系统类加载器。
那ExtensionLoader
是由哪个类加载器加载的呢?如果我们直接运行dubbo源码,那肯定是App类加载器了。如果我们是在项目中引入dubbo的jar包,那就不一样了。如果是springboot
打成的fatjar
,那就是springboot
自定义的类加载器了。
loadResource
读取文件内容,这段代码也挺简单,逐行读取内容,如果有#号
取#号
前面的内容。然后把每一行按=号
分隔,前面是name
,后面是类名。然后,把name
作为key
,把类名加载成类作为value
,放入extensionClasses
这个map中。
loadClass
首先判断传进来的类,是否实现了type
这个接口,如果没有实现则抛出异常。再看这个类是否存在Adaptive
注解,如果存在,则缓存一下。判断这个类是否是一个Wrapper
类,判断逻辑就是查看这个类是否存在一个含有type接口的构造函数。我们看到一个获取构造方法的代码clazz.getConstructor()
并没有变量接收这是为啥?
这是因为这些扩展点类,需要一个无参的构造函数,如果没有的话,会直接报错。
如果这个类没有对应的name,则查看这个类有没有Extension
注解,有的话取这个注解的值作为name,如果没有则用小写的类名。
例如下面这个示例,不过这种方式已经不推荐使用
了。
@Extension("yoyo")
public class YoYo implements Mascot {
@Override
public String getName() {
return "YoYo";
}
}
复制代码
继续,saveInExtensionClass
方法的作用是,把name和对应的类放入extensionClasses
map中。
cacheActivateClass
方法是判断创建的类有没有Activate
注解,如果有的话,则放到cachedActivates
缓存中。
关于Activate
注解后面再详细的介绍。
后记
本篇文章,主要介绍了Dubbo是如何解析META-INF
文件夹下接口对应文件的,对解析过程做了简单的源码介绍。
1.Mysql完结汇总篇(18W字送给大家),完结撒花
2.如何啃下JVM这座大山,完结撒花(完结篇)
3.最全的八股文线程池总结(臭不要脸)
4.手把手教新人调优
5.上班摸鱼学习法
6.阻塞队列yyds
7.线程与锁