Dubbo高级应用以及源码解析

开篇

本文主要介绍一些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);
    }
}
复制代码

查看执行结果

d2086fe56b702371686931f56f230493.jpeg
image.png

代码中的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就可以调用服务,不过书写起来不叫麻烦。需要指定generictrue

@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());
        }
    }
}
复制代码

右键运行,结果如下:

9c508db824cc541a645379451491dff6.jpeg
image.png

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支持多种协议:dubbohttpredisrmirestthrift等。我们需要哪些协议,只需要在配置文件里指定就好了,这就是通过Dubbo SPI来实现的。

下面我们来看下一段获取http协议的代码:

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getExtension("dubbo");
System.out.println(protocol);
复制代码

其中Protocol是一个协议接口,dubbo支持的协议都实现了这个接口。ExtensionLoader是一个扩展点加载器,上面例子第一行是说,获取一个Protocol的扩展点的扩展点加载器,dubbohttpredis这些协议就是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函数,并运行看一下吧。

d0740dd575a8f9b9343b159646a4f03f.jpeg
image.png

我们指定了,要获取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吗?来看一下:

e389686d5cc54284e02163af2d097289.jpeg 我们可以看到,虽然我们获取的是yoyo,但实际上输出的是MascotWrapper,但MascotWrapper里面的mascot属性值却是yoyo,这就是Dubbo SPI提供的类似AOP的功能,或者叫做依赖注入。当然,如果你愿意,也可以创建多个Wrapper,比如MascotWrapperWrapperMascotWrapperWrapperWrapper。。。

另外,这个命名也不一定飞叫Wrapper,名字是可以随便起的,你也可以叫MascotRap,dubbo是根据这个类的构造器来确定的。

源码分析

ExtensionLoader<Mascot> extensionLoader 
    = ExtensionLoader.getExtensionLoader(Mascot.class);
复制代码

这一行代码比较简单,获取Mascot接口对应的扩展点加载器。

72c34082de0ae09005e65cf86170f581.jpeg
image.png

getExtensionLoader方法,首先是一堆判断,可以先忽略,先从map缓存里获取接口类对应的扩展点加载器,如果缓存里没有,那就创建一个,然后放到缓存里,再返回。

Mascot yoyo = extensionLoader.getExtension("yoyo");
复制代码

这一行代码就是要获取一个具体的扩展点了,看下具体的实现。这段代码也比较简单,先判断是否为默认扩展点,如果是返回默认的扩展点,否则,判断缓存中是否已经创建了扩展点,如果没有创建过则创建对应的扩展点并放入缓存中,然后返回。

f7380f40d7de9d7c2e28dad7267ee4d5.jpeg
image.png

如果name的值是true,将得到一个默认的扩展点,那什么是默认的扩展点呢?我们来试一下。

首先对接口进行改造:

@SPI("yoyo")
public interface Mascot {
    String getName();
}
复制代码

把那name值改为true,运行一下,我们可以看到依然获取到了YoYo这个默认的对象。65025b024b836f98a434343f3ee18760.jpeg

继续看源码,getOrCreateHolder获取class对应的holder对象,这个对象作为一个锁,跟并发有关系,防止重复生成。

createExtension方法是创建扩展点。

eb7bed3b7192cb6bbf5b5066eb35a7ce.jpeg
image.png

getExtensionClasses方法是获取解析配置文件,查找所有的扩展点并缓存起来。查找配置文件的路径为META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/。查找到之后,把对应的key和类class文件放到map中。再根据传进来的name获取对应的class文件,并进行实例化。

injectExtension(instance);
复制代码

对实例化后的对象,看里面有哪些属性并进行赋值。

0a5e0f7cac9ffdd2dccf8431da0c60ef.jpeg
image.png

这里就是我们刚才写的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;
}
复制代码

这段代码主要干了两件事:

  1. 缓存默认扩展点,也就是@SPI的值作为默认扩展点。

  2. 根据某种策略来读取对应的文件夹。

  3. 读取文件里的文件找到类,访问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工程里。

b0a20be5582e55a7b9a8837b2f0e1d1f.jpeg 果然是,来看下里面的内容:

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

14dd1576f5c5da3249e23229844bc3dd.jpeg
image.png

从代码中看到,先把上面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
0b96d41f2e42b56a2ea5b2b09464d2e3.jpeg
image.png

读取文件内容,这段代码也挺简单,逐行读取内容,如果有#号#号前面的内容。然后把每一行按=号分隔,前面是name,后面是类名。然后,把name作为key,把类名加载成类作为value,放入extensionClasses这个map中。

loadClass
8dfb1886095954e738413f553f322d1f.jpeg
image.png

首先判断传进来的类,是否实现了type这个接口,如果没有实现则抛出异常。再看这个类是否存在Adaptive注解,如果存在,则缓存一下。判断这个类是否是一个Wrapper类,判断逻辑就是查看这个类是否存在一个含有type接口的构造函数。我们看到一个获取构造方法的代码clazz.getConstructor()并没有变量接收这是为啥?

这是因为这些扩展点类,需要一个无参的构造函数,如果没有的话,会直接报错。

如果这个类没有对应的name,则查看这个类有没有Extension注解,有的话取这个注解的值作为name,如果没有则用小写的类名。

例如下面这个示例,不过这种方式已经不推荐使用了。

@Extension("yoyo")
public class YoYo implements Mascot {
    @Override
    public String getName() {
        return "YoYo";
    }
}
复制代码

继续,saveInExtensionClass方法的作用是,把name和对应的类放入extensionClassesmap中。

cacheActivateClass方法是判断创建的类有没有Activate注解,如果有的话,则放到cachedActivates缓存中。

关于Activate注解后面再详细的介绍。

后记

本篇文章,主要介绍了Dubbo是如何解析META-INF文件夹下接口对应文件的,对解析过程做了简单的源码介绍。

1.Mysql完结汇总篇(18W字送给大家),完结撒花

2.如何啃下JVM这座大山,完结撒花(完结篇)

3.最全的八股文线程池总结(臭不要脸)
4.手把手教新人调优
5.上班摸鱼学习法
6.阻塞队列yyds
7.线程与锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值