前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深度Dubbo源码 - SPI的使用与好处

深度Dubbo源码 - SPI的使用与好处

作者头像
RedSheep
修改2019-10-31 22:19:37
6470
修改2019-10-31 22:19:37
举报
文章被收录于专栏:技术男的宅之路

背景

相信阅读过Dubbo源码的同学应该看到在Dubbo中的很多接口上都有一个 @SPI的注解,笔者也不例外,但是一直不知道这个注解具体是干什么的,为了解决什么问题,怎么去使用?网上简单检索了下,中文名:服务供给接口,详见下图(来自百度百科)。

也许因为 dubbo本身的功能强大,所以笔者也只是知道能 dubbo可以自定义实现某些策略,比如负载均衡、序列化、线程池类型等等,但是还未正式在线上环境中使用。趁着节假日花些时间研究下,记录下,希望对大家有用。

代码样例

以下代码均是经过本地验证的,纯属手敲,具体执行详见https://github.com/GloryXu/spring-boot

注:测试项目搭建 spring-boot + spring-boot-dubbo(https://github.com/apache/dubbo-spring-boot-project)

验证思路

正如上图所述,说 @SPI是实现某个特定的服务,那就来个简单的实现,最熟悉的莫过于负载均衡( LoadBalance)策略了,本地启动两个 provider,端口不同,通过 consumer的入参来决定访问指定的 provider

启动provider

代码极其简单,代码框架如下

代码语言:javascript
复制
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 指定版本和分组
@Service(version = "1.0.0",group = "glory")
public class DemoServiceImpl implements DemoService {
    private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);

    @Override
    public String sayHello(Integer port) {
        logger.info("Hello " + port + " request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return "Hello ,"+port+" response from provider: " + RpcContext.getContext().getLocalAddress();
    }

}

以下为 application.yml配置文件

代码语言:javascript
复制
server:
  port: 8083
dubbo:
  application:
    name: dubbo-common-provider
  scan:
    base-packages: com.redsun.rpc.dubbo
  protocol:
    name: dubbo
    port: 12345
  registry:
    address: zookeeper://127.0.0.1:2181

还需要注意的是,在启动时需要指定不同端口,否则无法启动。

此时就能正常启动两个本地应用了,启动效果如下:

启动consumer

consumer的代码框架也很简单

想必大家猜也能看出来, GloryLoadBalance就是自定义实现的一种负载均衡策略,通过前端传入的参数来选择 invoker

代码语言:javascript
复制
public class GloryLoadBalance implements LoadBalance {

    private static final Logger logger = LoggerFactory.getLogger(GloryLoadBalance.class);

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        Integer port = (Integer) invocation.getArguments()[0];// 前端传入的参数
        logger.info("前端传入端口为:" + port);
        // java8的流式编程,有兴趣的同学可以研究下,后续会再专门写一篇
        Invoker<T> invokerRet = invokers.stream().filter(invoker -> invoker.getUrl().getPort() == port).findFirst().get();
        return invokerRet == null ? invokers.get(0) : invokerRet;
    }
}

此处还简单写了个 controller,方便动态改参数

代码语言:javascript
复制
@RestController
@RequestMapping("/admin")
public class AdminController {
    private static final Logger logger = LoggerFactory.getLogger(AdminController.class);

    @Reference(version = "1.0.0",group = "glory", loadbalance = "glory")
    private DemoService demoService;

    @RequestMapping("/invoke")
    public String invoke(@RequestParam(name = "port") Integer port) {
        logger.info("invoke method be invoked!port = " + port);
        return demoService.sayHello(port);
    }
}

当然还会最后一步,也是最重要的,在 META-INF.dubbo.internal目录添加配置文件,下面等号前面的 glory其实就是你配置的 loadbalance 的key,如果路径错了,或者未配置,还是会获取到默认的实现 random

代码语言:javascript
复制
glory=com.redsun.rpc.dubbo.loadbalance.GloryLoadBalance

dubbo会加载 META-INF.dubbo.internal目录中的所有配置信息,在 dubbo目录下也会有很多的默认实现

Postman调用

以下两张图测试,满足预期。

源码

下面就到了翻源码的时候了,这里简单讲一下我本人看源码的一点心得。

  • 别纠结于每个方法的实现,一般都是debug跟,很容易跟晕,最后就失去了兴趣
  • 学会看注释,尤其是方法名、类名、变量名,源码不同于平时自己编写的代码,既然开源了,就是面向所有人的,所以注释、起名往往会写的很详细很规范,英文不认识?翻译看,次数多了也就认识了
  • 在debug跟的时候记住几个核心的类,看完之后梳理下整个的调用链,对代码结构先要有个大概的认知(实在不想看,百度也行,我也经常这么干,然后再自己跟一下,认证下)

加载

ExtensionLoader#getExtensionClasses

代码语言:javascript
复制
// synchronized in getExtensionClasses
    private Map<String, Class<?>> loadExtensionClasses() {
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }
代码语言:javascript
复制
/**
     * put clazz in extensionClasses
     */
    private void saveInExtensionClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) {
        Class<?> c = extensionClasses.get(name);
        if (c == null) {
            // 将扩展类GloryLoadBalance存入map中,最终会将默认提供的几种都存入map中
            extensionClasses.put(name, clazz);
        } else if (c != clazz) {
            throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + name + " on " + c.getName() + " and " + clazz.getName());
        }
    }

加载好实现类最终会将 ReferenceConfig的负载均衡参数设置为 glory

运行

AbstractClusterInvoker#invoke

consumer发起 invoke的时候会根据config的key进行有选择的创建实例

代码语言:javascript
复制
public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();

        // binding attachments into invocation.
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            ((RpcInvocation) invocation).addAttachments(contextAttachments);
        }

        List<Invoker<T>> invokers = list(invocation);
        // 初始化加载负载均衡类
        LoadBalance loadbalance = initLoadBalance(invokers, invocation);
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        return doInvoke(invocation, invokers, loadbalance);
    }

ExtensionLoader#getExtensionLoader

每个 META-INF.dubbo.internal目录中的文件都是 ExtensionLoader对象,存储在一个静态的类变量 EXTENSION_LOADERS

代码语言:javascript
复制
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        // 此处判断是否以SPI注解修饰
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }

        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

ExtensionLoader#getExtension

如果此时为空,则会通过上面加载的Class信息newInstance一个实例使用,代码比较简单,有兴趣的可以跟一下。

总结

通过以上应该可以看出,后续如果想扩展使用dubbo会非常的简单,增加一个实现类(实现对应接口),再在 META-INF目录下添加一个配置文件,key对应好就能完成了。序列化方式也可以按照自己的意愿来。在跟代码的时候也能看到对于一些其他不使用的扩展类,dubbo都将Class对象加载进去了,也算是一点点小瑕疵吧。

觉得有帮助,欢迎关注
觉得有帮助,欢迎关注
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爪哇之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 代码样例
    • 验证思路
      • 启动provider
        • 启动consumer
          • Postman调用
          • 源码
            • 加载
              • ExtensionLoader#getExtensionClasses
            • 运行
              • AbstractClusterInvoker#invoke
              • ExtensionLoader#getExtensionLoader
              • ExtensionLoader#getExtension
          • 总结
          相关产品与服务
          负载均衡
          负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档