• 作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列
  • 如果感觉博主的文章还不错的话,请三连支持一下博主哦
  • 博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬、

文章目录

    • 一、引言
    • 二、环境配置
    • 三、服务暴露
      • 1、判断注册方式
        • 1.1 获取注册的URL
        • 1.2 应用注册
        • 1.3 接口注册
      • 2、服务导出
        • 2.1 服务暴露
        • 2.2 动态代理生成
        • 2.3 服务导出
          • 2.3.1 服务启动
          • 2.3.2 注册Zookeeper
      • 3、疑惑解答
    • 四、总结

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、环境配置

本篇文章适合对 dubbo 有兴趣 & 日常工作中有使用的人

环境配置:

  • dubbo版本:3.1.8
  • maven版本:3.5.4
  • JDK版本:JDK8
  • Zookeeper版本:3.4.9

因为服务数据是注册在 Zookeeper 上的,所以需要一个 Zookeeper 的可视化界面:ZooInspector

当然,就算你上述环境配置不全,也不影响你本篇文章的阅读体验。

三、服务暴露

上一篇文章《从源码全面解析dubbo服务注册的来龙去脉》 我们分析了我们的 Dubbo 是如何解析 @EnableDubboConfig@DubboComponentScan 这两个注解的

我们留了一个 服务暴露 的过程没有讲。因为 服务暴露Dubbo 中属于比较重要的知识点,所以单独一篇来进行讲解

我们上一篇文章讲到在 DubboDeployApplicationListener 里面实现了 服务暴露 的过程

我们主要讲一下 DubboDeployApplicationListener 的实现:

public class DubboDeployApplicationListener implements ApplicationListener<ApplicationContextEvent>, ApplicationContextAware, Ordered {@Overridepublic void onApplicationEvent(ApplicationContextEvent event) {if (nullSafeEquals(applicationContext, event.getSource())) {// 判断当前的事件if (event instanceof ContextRefreshedEvent) {// 刷新事件onContextRefreshedEvent((ContextRefreshedEvent) event);} else if (event instanceof ContextClosedEvent) {// 关闭事件onContextClosedEvent((ContextClosedEvent) event);}}}}private void onContextRefreshedEvent(ContextRefreshedEvent event) {ModuleDeployer deployer = moduleModel.getDeployer();Future future = deployer.start();}public Future start() throws IllegalStateException {// 初始化模块applicationDeployer.initialize();return startSync();}private synchronized Future startSync() throws IllegalStateException {// 服务暴露exportServices();}

1、判断注册方式

由于我们这个是 Dubbo3 的源码,Dubbo3 新增了一种注册方式:应用级注册,所以在这里会判断当前的注册方式是哪一种

  • dubbo2:接口级注册
  • dubbo3:应用级注册

我们看一下 exportServices 是如何对其进行处理的

直接来到 org.apache.dubbo.config.ServiceConfigdoExport 方法

protected synchronized void doExport() {// 将一个服务暴露成多个URLdoExportUrls();exported();}

1.1 获取注册的URL

private void doExportUrls() {// 获取当前List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);// 省略部分代码}public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {// 获取配置里面的地址String address = config.getAddress();if (StringUtils.isEmpty(address)) {address = ANYHOST_VALUE;}// 若配置多个URL地址,在这里切割List<URL> urls = UrlUtils.parseURLs(address, map);// 填充URL多个地址for (URL url : urls) {// 填充URL地址url = URLBuilder.from(url).addParameter(REGISTRY_KEY, url.getProtocol()).setProtocol(extractRegistryType(url)).setScopeModel(interfaceConfig.getScopeModel()).build();// 判断当前是不是服务端,将URL添加至registryListif (provider || url.getParameter(SUBSCRIBE_KEY, true)) {registryList.add(url);}// 这里的URL示例:// registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService" />}// 根据注册类型拼接URL配置return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);}

1.2 应用注册

  • 当前 registerMode(注册类型)instance 或者 all 时,走应用注册
if ((DEFAULT_REGISTER_MODE_INSTANCE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode))&& registryNotExists(registryURL, registryList, SERVICE_REGISTRY_PROTOCOL)) {URL serviceDiscoveryRegistryURL = URLBuilder.from(registryURL).setProtocol(SERVICE_REGISTRY_PROTOCOL).removeParameter(REGISTRY_TYPE_KEY).build();result.add(serviceDiscoveryRegistryURL);}
  • URL:

    service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

1.3 接口注册

  • 当前 registerMode(注册类型)interface 或者 all 时,走接口注册

    if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) {result.add(registryURL);}
  • URL

    registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

2、服务导出

// 遍历当前的传输协议(dubbo、rest、tri)for (ProtocolConfig protocolConfig : protocols) {// 组装当前的接口URL:com.common.service.IUserService:1.0.0.devString pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);// 正式导出服务doExportUrlsFor1Protocol(protocolConfig, registryURLs);}private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {// 获取数据配置Map<String, String> map = buildAttributes(protocolConfig);serviceMetadata.getAttachments().putAll(map);// 组装URLURL url = buildUrl(protocolConfig, map);// 服务暴露exportUrl(url, registryURLs);}
  • 组装的URL:

    dubbo://192.168.0.103:20883/com.common.service.IUserService" />2.1 服务暴露 
    private void exportUrl(URL url, List<URL> registryURLs) {exportLocal(url);}private void exportLocal(URL url) {// 组装配置URL local = URLBuilder.from(url).setProtocol(LOCAL_PROTOCOL).setHost(LOCALHOST_VALUE).setPort(0).build();local = local.setScopeModel(getScopeModel()).setServiceModel(providerModel);// 服务暴露doExportUrl(local, false);}private void doExportUrl(URL url, boolean withMetaData) {// 生成动态代理Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);if (withMetaData) {invoker = new DelegateProviderMetaDataInvoker(invoker, this);}// 服务导出Exporter<?> exporter = protocolSPI.export(invoker);exporters.add(exporter);}

    2.2 动态代理生成

    这个里面的 getIUnvoker 很经典的动态代理模式,当我们客户端调用时,会调用 doInvoke 里面的方法

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);return new AbstractProxyInvoker<T>(proxy, type, url) {@Overrideprotected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) {// proxy:UserServicelmpl// 直接调用实现类return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);}};} 

    2.3 服务导出

    Exporter<?> exporter = protocolSPI.export(invoker);public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {// 暴露服务final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);// 获取注册的Registryfinal Registry registry = getRegistry(registryUrl);final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);// 注册方式的选择boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);if (register) {// 这里有两种注册情况:// 1、接口注册:直接将注册数据注册到Zookeeper即可// 2、应用注册:将注册数据转换成元数据等后面发布元数据register(registry, registeredProviderUrl);}}
    2.3.1 服务启动

    我们都知道 dubbo 都是自定义的端口,比如上面的我们的 20883,这个端口哪里来的呢?

    相信有部分同学可能猜到答案了,没错,就是 Netty 启动的

    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    我们直接跳转到 DubboProtocolexport 方法

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {// 启动服务openServer(url);optimizeSerialization(url);return exporter;}private void openServer(URL url) {checkDestroyed();String key = url.getAddress();boolean isServer = url.getParameter(IS_SERVER_KEY, true);if (isServer) {ProtocolServer server = serverMap.get(key);// 典型的双端检锁机制if (server == null) {synchronized (this) {server = serverMap.get(key);if (server == null) {// createServer:启动服务serverMap.put(key, createServer(url));return;}}}// 出现问题重置服务配置server.reset(url);}}

    创建服务端:

    private ProtocolServer createServer(URL url) {// 数据组装url = URLBuilder.from(url).addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString()).addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT)).addParameter(CODEC_KEY, DubboCodec.NAME).build();String transporter = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);ExchangeServer server = Exchangers.bind(url, requestHandler);}

    直接跳到 NettyServerdoOpen 方法

    protected void doOpen() throws Throwable {bootstrap = new ServerBootstrap();bossGroup = createBossGroup();workerGroup = createWorkerGroup();final NettyServerHandler nettyServerHandler = createNettyServerHandler();channels = nettyServerHandler.getChannels();initServerBootstrap(nettyServerHandler);ChannelFuture channelFuture = bootstrap.bind(getBindAddress());channelFuture.syncUninterruptibly();channel = channelFuture.channel();}
    2.3.2 注册Zookeeper

    我们这里直接跳到 ZookeeperRegistrydoRegister 方法

    public void doRegister(URL url) {// 校验checkDestroyed();// 将接口信息注册至ZookeeperzkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);}// URL:dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

    我们看下 Zookeeper 的前后对比:

    前:

    后:

    这时候拿出我们的UnCode转换器将乱码进行转换:

    dubbo://192.168.0.103:20883/com.common.service.IUserService" />3、疑惑解答 

    我们可以看到,我们第一次获取注册 ZookeeperURL 是:

    这个 127.0.0.1:2181 是我们当前 Zookeeper 的地址,

    service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

    通过该 URL 我们可以和我们的 Zookeeper 进行一些相互

    第二次拿到的 URL

    这个 192.168.0.103:20883 是我们 Netty 服务器暴露的地址,将该地址注册至 Zookeeper,便于消费者的访问

    dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

    四、总结

    鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

    其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

    如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

    我是爱敲代码的小黄,独角兽企业的 Java 开发工程师,CSDN 博客专家,喜欢后端架构和中间件源码。

    我们下期再见。

    我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

    往期文章推荐:

    • 不亏是阿里三面,ConcurrentHashMap多线程扩容机制被面试官装到了
    • 美团二面:聊聊ConcurrentHashMap的存储流程
    • 从源码全面解析Java 线程池的来龙去脉
    • 从源码全面解析LinkedBlockingQueue的来龙去脉
    • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
    • 从源码全面解析ReentrantLock的来龙去脉
    • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
    • 从源码全面解析 ThreadLocal 关键字的来龙去脉
    • 从源码全面解析 synchronized 关键字的来龙去脉
    • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试