并不简单的代理,Dubbo是如何做服务引用的

系列文章目录

【收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制
面试Dubbo ,却问我和Springcloud有什么区别?
超简单,手把手教你搭建Dubbo工程(内附源码)
Dubbo最核心功能——服务暴露的配置、使用及原理


不满足于RPC,Dubbo是如何做服务引用的

  • 系列文章目录
  • 前言
  • 一、服务引用的介绍
  • 二、配置与模型
    • 1. 服务引用配置
    • 2. 整体实现模型
  • 三. 引用的具体实现
    • 1. 触发创建代理对象
    • 2. 创建代理的实现
    • 3. 代理的获取
  • 四、小结


前言

我们在Dubbo与SpringCloud对比的那一期就说过,Dubbo并不满足于作为RPC框架,除了RPC所需要的内容,还提供了如服务治理等额外的支持。但是无奈,很多人对Dubbo的印象依旧停留在RPC框架。所以,我们在学习的初期,我们先不急着学习那些分支内容,还是专注其核心,即围绕RPC循序渐进的学习。那么上一篇我们讲了服务暴露,今天就一起来学习RPC桥梁的另一端:消费者,即服务的引用。当然,在此之前,还是希望大家带着问题来学习,比如说:

  • 服务引用在底层替我们做了什么?
  • Spring怎么和服务引用融合的?

📕作者简介:战斧,多年开发及管理经验,爱好广泛,致力于创作更多高质量内容
📗本文收录于 Dubbo专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待

一、服务引用的介绍

在JAVA中,引用的概念是一直贯穿的。大部分情况,我们都是进行着对象引用。但是通过类比,我们不难得出服务引用的概念:服务引用是指在程序中使用另一个程序或服务提供的功能或数据的方式。通过服务引用,程序可以调用其他程序或服务提供的功能,获取返回值并将其用于自己的业务逻辑中

二、配置与模型

1. 服务引用配置

同服务暴露一样,实现服务引用的可以通过XML文件和注解两种途径来实现
XML 文件:

<dubbo:reference  interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" timeout="2000"/>

或者 @DubboReference 注解

@Component
public class Consumer implements CommandLineRunner {@DubboReference(timeout = 2000)private DemoService demoService;@Overridepublic void run(String... args) throws Exception {String result = demoService.sayHello("战斧");System.out.println("收到消息:" + result);}
}

2. 整体实现模型

如果我们做了上面的配置,那么它是如何实现调用远方或本地的服务的呢?我们先简单概括一下:Dubbo 为我们的引用创建了一个代理对象,这个代理对象如同一个电话筒,当我们对电话筒说话的时候,它会将我们的话传到指定的人那里。同理,当我们调用这个代理对象时,它会把我们的调用请求传递到服务提供方那里,获得结果后,再反馈给我们当然,这里仅仅是将其简单化成一个代理对象,实际其结合了容错策略后,实现还是很复杂的
在这里插入图片描述

三. 引用的具体实现

如同前几期的一样,我们今天仍然以与Spring框架结合下的Dubbo进行分析

1. 触发创建代理对象

服务引用的操作其实和服务暴露一样,因此把上次服务暴露的图稍微改下各位就明白了
在这里插入图片描述
与服务暴露仅有后置处理器不同:

服务暴露的工厂后置处理器为ServiceAnnotationPostProcessor ,而现在服务引用的工厂后置处理器为ReferenceAnnotationBeanPostProcessor,当然,其扫描的注解也有不同,服务引用扫描的注解为

  1. org.apache.dubbo.config.annotation.DubboReference
  2. org.apache.dubbo.config.annotation.Reference
  3. com.alibaba.dubbo.config.annotation.Reference

至于容器刷新监听,其实都是由 DubboDeployApplicationListener 进行监听,最终也都会落在在同一个方法里
onContextRefreshedEvent -> deployer.start() -> startSync()

// DefaultModuleDeployer.class
private synchronized Future startSync() throws IllegalStateException {if (isStopping() || isStopped() || isFailed()) {throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");}try {......// 服务暴露exportServices();......// 引用服务referServices();......} catch (Throwable e) {onModuleFailed(getIdentifier() + " start failed: " + e, e);throw e;}return startFuture;
}

接下来,就让我们专注于真正的引用服务方法 DefaultModuleDeployer.referServices()

// DefaultModuleDeployer
private void referServices() {configManager.getReferences().forEach(rc -> {try {ReferenceConfig<?> referenceConfig = (ReferenceConfig<?>) rc;if (!referenceConfig.isRefreshed()) {referenceConfig.refresh();}// 每一个rc 其实就是一个引用配置,比如我们在DemoService上加了@DubboReference注解,就会被解析成一个引用配置,形如// <dubbo:reference sticky="false" timeout="2000" id="demoService" interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" />if (rc.shouldInit()) {if (referAsync || rc.shouldReferAsync()) {ExecutorService executor = executorRepository.getServiceReferExecutor();CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {referenceCache.get(rc);} catch (Throwable t) {logger.error(CONFIG_FAILED_EXPORT_SERVICE, "", "", "Failed to async export service config: " + getIdentifier() + " , catch error : " + t.getMessage(), t);}}, executor);asyncReferringFutures.add(future);} else {// 从引用缓存中尝试获取该引用配置的代理对象,注意此处仅仅执行该方法,并没有获取其返回值referenceCache.get(rc);}}} catch (Throwable t) {logger.error(CONFIG_FAILED_REFERENCE_MODEL, "", "", "Model reference failed: " + getIdentifier() + " , catch error : " + t.getMessage(), t);referenceCache.destroy(rc);throw t;}});
}

上文中的 referenceCache 其实是一个标注了 final 的 SimpleReferenceCache 对象,其储存着所有的引用代理的情况,需要注意的是,真正的代理对象并不存储在它这里。

public <T> T get(ReferenceConfigBase<T> rc) {// 为该配置生成key, 即接口的全限定名,此处就是com.zhanfu.dubbo.demo.dubbo.api.DemoServiceString key = generator.generateKey(rc);// 接口的类型 com.zhanfu.dubbo.demo.dubbo.api.DemoService.calssClass<?> type = rc.getInterfaceClass();boolean singleton = rc.getSingleton() == null || rc.getSingleton();T proxy = null;// 单例类才需要从缓存取,不然每次都得新取一个if (singleton) {proxy = get(key, (Class<T>) type);} else {logger.warn(CONFIG_API_WRONG_USE, "", "", "Using non-singleton ReferenceConfig and ReferenceCache at the same time may cause memory leak. " +"Call ReferenceConfig#get() directly for non-singleton ReferenceConfig instead of using ReferenceCache#get(ReferenceConfig)");}// 第一次或者不是单例,创建代理信息if (proxy == null) {// 代理对象并不存储,存储的是引用与引用配置的映射List<ReferenceConfigBase<?>> referencesOfType = referenceTypeMap.computeIfAbsent(type, _t -> Collections.synchronizedList(new ArrayList<>()));referencesOfType.add(rc);List<ReferenceConfigBase<?>> referenceConfigList = referenceKeyMap.computeIfAbsent(key, _k -> Collections.synchronizedList(new ArrayList<>()));referenceConfigList.add(rc);// 真正创建代理的地方,还是由引用配置对象自己来实现的proxy = rc.get();}return proxy;
}

至此,结合Spring的项目,我们对服务引用的准备就完成了,接下来就是其核心内容——代理的创建

2. 创建代理的实现

紧接上文,我们看到了 rc.get() ,其实这里就是真正创建代理的地方

// ReferenceConfig.class
public T get() {if (destroyed) {throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");}if (ref == null) {// 确保模块已启动,兼容老版本getScopeModel().getDeployer().start();synchronized (this) {if (ref == null) {init();}}}return ref;
}

init() 方法内容较多,我们仅看关键的一行,此处的referenceParameters包含了引用所需要的各种信息

 ref = createProxy(referenceParameters);

在这里插入图片描述
那么我们再进入 createProxy 一探究竟

ProxyFactory proxyFactory = this.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
private T createProxy(Map<String, String> referenceParameters) {// 创建本地调用器if (shouldJvmRefer(referenceParameters)) {createInvokerForLocal(referenceParameters);} else {urls.clear();meshModeHandleUrl(referenceParameters);if (StringUtils.isNotEmpty(url)) {// user specified URL, could be peer-to-peer address, or register center's address.parseUrl(referenceParameters);} else {// if protocols not in jvm checkRegistryif (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {aggregateUrlFromRegistry(referenceParameters);}}// 创建远程调用器createInvokerForRemote();}URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,referenceParameters.get(INTERFACE_KEY), referenceParameters);consumerUrl = consumerUrl.setScopeModel(getScopeModel());consumerUrl = consumerUrl.setServiceModel(consumerModel);// 发布订消费者元信息,注册到注册中心上MetadataUtils.publishServiceDefinition(consumerUrl, consumerModel.getServiceModel(), getApplicationModel());// 创建引用代理,此处的 proxyFactory 是Dubbo 的SPI接口,不是Spring提供的那个类return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}

在这里插入图片描述
可以看到这又是一个自适应的代理工厂类,那么其具体采用那个实现类,就要看入参了,此刻我们采用的远程服务,使用的MigrationInvoker ,最终使用的是默认的 ”javassist“ ,即 org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory 实现类,并最终拼接成操作一个代理类,并将其实例化
在这里插入图片描述
在这里插入图片描述
这中间通过javaassit生成的过程十分繁杂,我们直接将生成的代理进行反编译,其代码如下

class DemoServiceDubboProxy0
{public DemoServiceDubboProxy0();public DemoServiceDubboProxy0(java.lang.reflect.InvocationHandler);public void $destroy();public java.lang.String sayHello(java.lang.String);public java.lang.Object $echo(java.lang.Object);public static [Ljava.lang.reflect.Method; methods;private java.lang.reflect.InvocationHandler handler;
}

因此,我们知道了,代理内其实还是一个 MigrationInvoker 在执行操作,但是我们现在并不看其内部在干什么,留待讲调用的章节再来详谈,因此点到为止,完成了一个代理对象的创建。

3. 代理的获取

完成了代理对象的创建,工作并没有结束,我们在业务代码中,还需要获取代理。当然在Spring框架下,更贴切的叫法叫做注入。把代理对象注入我们业务代码中。如下图
在这里插入图片描述

我们首先要知道的是通过xml 或@DubboReference 注解标志的接口,最终会被解析成一个 ReferenceBean 对象。

public class ReferenceBean<T> implements FactoryBean<T>,ApplicationContextAware, BeanClassLoaderAware, BeanNameAware, InitializingBean, DisposableBean

因为其实现了FactoryBean 接口,所以我们应当能猜到重点并不是其本身,而是通过它的 getObject() 方法能获得什么东西。 (不了解FactoryBean 的看这里: BeanFactory 和 FactoryBean 的关联与区别)

public T getObject() {if (lazyProxy == null) {createLazyProxy();}return (T) lazyProxy;
}

果不其然,我们想要的东西很快就出现了,那就是代理对象的创建,我们关注到,这里其实提供的是一个惰性代理,这种设计是为了防止一些Bean过早产生,因为当时它的配置和环境未必已经准备完成,此时直接初始化该Bean可能会出现各种问题。(其实我们在开发中,也有很多时候会使用@Lazy 注解来达到类似的目的)那么这个代理是如何创建的呢?

private void createLazyProxy() {// 使用的是Spring提供的代理工厂ProxyFactory proxyFactory = new ProxyFactory();proxyFactory.setTargetSource(new DubboReferenceLazyInitTargetSource());proxyFactory.addInterface(interfaceClass);// 代理还需要实现一些额外的接口,此处是 EchoService.class, Destroyable.classClass<?>[] internalInterfaces = AbstractProxyFactory.getInternalInterfaces();for (Class<?> anInterface : internalInterfaces) {proxyFactory.addInterface(anInterface);}if (!StringUtils.isEquals(interfaceClass.getName(), interfaceName)) {//add service interfacetry {Class<?> serviceInterface = ClassUtils.forName(interfaceName, beanClassLoader);proxyFactory.addInterface(serviceInterface);} catch (ClassNotFoundException e) {// generic call maybe without service interface class locally}}// 创建代理this.lazyProxy = proxyFactory.getProxy(this.beanClassLoader);
}

我们这里要注意一个重点,我们为 proxyFactory 代理工厂设置了一个目标对象 DubboReferenceLazyInitTargetSource,熟悉 SpringAop 的朋友应该知道 proxyFactory 的目标对象就是真正执行方法的对象,代理只是在调用该对象的前后加入了一些操作。那么我们来看看 DubboReferenceLazyInitTargetSource 有什么特殊之处

// ReferenceBean 的内部类
private class DubboReferenceLazyInitTargetSource extends AbstractLazyCreationTargetSource {@Overrideprotected Object createObject() throws Exception {return getCallProxy();}@Overridepublic synchronized Class<?> getTargetClass() {return getInterfaceClass();}
}

我们可以看到该对象最大的特点是其实现了 AbstractLazyCreationTargetSource ,两个方法也是重写的父类的。限于主题与篇幅,我们此处不针对该父类详细展开,仅对其作用进行描述:

AbstractLazyCreationTargetSource是 Spring Framework 中的一个抽象类,它实现了
TargetSource 接口。它的主要作用是作为一个懒加载的代理对象,在第一次调用它时,通过回调方法创建并缓存目标对象。

换而言之,它就是一个临时工,正主有事它先占着位置,等到真正需要工作时它再去叫正主,换句话说,如果我们仅是获取代理,里面的目标对象就是这个,但是如果我们调用了代理的方法,它就会通过createObject方法来叫真正的对象,我们可以看到,它是通过getCallProxy来叫真正的对象

private Object getCallProxy() throws Exception {// 惰性代理的作用,只有当环境与配置都准备完毕,才会真正产生可用的代理目标if (referenceConfig == null) {throw new IllegalStateException("ReferenceBean is not ready yet, please make sure to call reference interface method after dubbo is started.");}// 对象的创建需要给容器上锁,放置死锁synchronized (((DefaultSingletonBeanRegistry)getBeanFactory()).getSingletonMutex()) {return referenceConfig.get();}
}
// ReferenceConfig ,即引用配置
public T get() {if (destroyed) {throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");}if (ref == null) {// ensure start module, compatible with old api usagegetScopeModel().getDeployer().start();synchronized (this) {if (ref == null) {init();}}}return ref;
}

在这里插入图片描述

到这里,我们终于看到了返回的代理对象,即上文里的ref,而ref的诞生,我们在上一个小节,创建代理对象里已经讲过了。至此,业务代码如何获取的代理我们也看完了。

四、小结

通篇下来,我们看了大量的源码,并辅以文字解释,总算讲完了Dubbo是如何做服务引用的,一句话概括就是使用了代理,但是这个代理的设计与实现却很复杂,总结有三个原因:

  • 全面的SPI自适应设计,使得代理工厂有好几个可选择
  • 本地或者远程,会创建不同的invoker,即调用器
  • 为了和Spring配合,使用了惰性代理避免过早的初始化而出错

目前我们已经把服务引用和服务暴露都讲了,虽然还有大量的细节有待补充,但其核心流程已经说完,相当于一座大桥的两头我们已经搭好,下一次,我们将进行这座大桥的合拢————即看看服务调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/22414.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django实现接口自动化平台(九)环境envs序列化器及视图【持续更新中】

相关文章&#xff1a; Django实现接口自动化平台&#xff08;八&#xff09;测试报告reports序列化器及视图【持续更新中】_做测试的喵酱的博客-CSDN博客 本章是项目的一个分解&#xff0c;查看本章内容时&#xff0c;要结合整体项目代码来看&#xff1a; python django vue …

Git详解

Git详解 认识GitGit的组成三个区域二大类&#xff0c;四种状态 Git add所做的操作Git commit所做的操作分支&#xff08;branch&#xff09;标签Tag Git checkout的作用创建分支切换分支切换提交快照撤销更改 Git安装Linux版本yum源方式安装编译方式安装 Windows Git命令使用比…

【电动车】基于多目标优化遗传算法NSGAII的峰谷分时电价引导下的电动汽车充电负荷优化研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

VisualStudio2022将printf信息打印到控制台

点击“解决方案管理器”&#xff0c;选中项目名称&#xff0c;点击鼠标右键---->属性---->生成事件---->生成后事件&#xff0c;在命令行的右侧输入框里填写如下内容&#xff1a; editbin /SUBSYSTEM:CONSOLE $(OUTDIR)\$(ProjectName).exe接下来在编译运行时&#x…

机器学习-sigmoid函数和relu函数-个人解读

机器学习-sigmoid函数和relu函数-个人解读 今天博主来解读一下sigmoid函数和relu函数&#xff0c;我觉得很多同学可能都知道这两个函数是什么&#xff0c;他们干什么的&#xff0c;他们有什么用&#xff0c;但是呢&#xff1f;我想这两个常用的激活函数内在的本质&#xff0c;…

【C++】模板(函数模板与类模板)讲解

本篇文章会对C中的模板进行讲解&#xff0c;其中会对函数模板和类模板进行讲解。希望本篇文章会对你有所帮助。 文章目录 一、函数模板 1、1 模板的引入 1、2 函数模板举例讲解 1、2、1 函数模板的概念 1、2、2 函数模板格式 1、2、3 函数模板实例化 1、2、4 模板参数的匹配原则…

如何将SAP数据抽取到Azure数据湖平台?

经过多年的发展&#xff0c;SNP Glue 在全球已成为值得信赖且广为人知的解决方案&#xff0c;支持客户将其 SAP 数据与现代化的平台集成。SNP Glue 打破了数据孤岛&#xff0c;向数据科学家开放了 SAP&#xff0c;支持基于企业 SAP 数据的多个新用例。 随着时间的推移&#xff…

详解 HTTPS、TLS、SSL、HTTP区别和关系

一、什么是HTTPS、TLS、SSL HTTPS&#xff0c;也称作HTTP over TLS。TLS的前身是SSL&#xff0c;TLS 1.0通常被标示为SSL 3.1&#xff0c;TLS 1.1为SSL 3.2&#xff0c;TLS 1.2为SSL 3.3。下图描述了在TCP/IP协议栈中TLS(各子协议)和HTTP的关系。 二、HTTP和HTTPS协议的区别 …

前端AES加密,后端解密,有效防止数据外泄

在工作中经常遇到密码明文传输这个问题&#xff0c;为了让密码安全些会让加密&#xff0c;现在有个比较方便的AES加密&#xff08;前端密钥可能存在泄露风险&#xff0c;应该放到配置项中&#xff09;&#xff1a; 一、前端加密 1、首先引入前端需要用到的js&#xff1a;crypt…

【Git】Git 拉取的快速方法(含项目示例)

文章目录 一、问题的提出二、问题的尝试解决 一、问题的提出 在我们之前的拉取中&#xff0c;速度可能比较慢&#xff0c;例如&#xff0c;我们要拉取CLIP的项目。 (ldm) rootI1385efcc2300601b29:/hy-tmp/latent-diffusion# pip install githttps://github.com/openai/CLIP.…

接口测试辅助,Fiddler抓取安卓手机https请求(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Fiddler 是一款免…

Qt + QR-Code-generator 生成二维码

0.前言 之前使用 libgrencode 生成二维码&#xff0c;LGPL 协议实在不方便&#xff0c;所以需要找一个 github 星星多的&#xff0c;代码简单最好 header-only&#xff0c;协议最好是 MIT 或者兼容协议而不是 GPL 或者 LPGL。 QR-Code-generator 正好符合这个要求&#xff0c…