深入理解Dubbo原理系列(三)- Dubbo服务暴露和消费原理
一. Dubbo服务暴露原理解析
不管在服务暴露还是在服务消费的场景下,Dubbo框架都会根据优先级来对配置信息做一个聚合处理, 目前默认的覆盖策略主要遵循以下几点规则:
- -D传递给JVM参数的优先级最高,比如
-Dubbo.protocol.port=20880
。 - 代码或XML配置优先级次高,比如Spring中XML文件指定
<dubbo:protocol port=“20880”>
。 - 置文件优先级最低,比如
dubbo.properties
文件指定dubbo.protocol.port=20880
。
此外,只有在XML没有配置的时候,dubbo.properties
配置项才会生效。Dubbo的配置也会受到provider的影响(运行期属性值影响),并遵循以下两点规则:
- 如果只有provider端指定配置,则会自动透传到客户端,比如
timeout
。 - 如果客户端也配置了相应属性,则服务端配置会被覆盖,比如
timeout
。
紧接着,来看下RPC框架的一个暴露原理:
整体来看,DUbbo的服务暴露部分分为两个部分:
- 将持有的服务实例通过代理转换成Invoker。
- 将Invoker通过具体的协议(比如Dubbo框架中的Dubbo协议)转换成Exporter。
我之前的文章提到过,Dubbo中的所有模型都会向Invoker靠拢,向它发起invoke调用,他的实现可能是本地、远程、集群三种,因此,本文服务暴露准备从远程和本地两个角度来讲。
1.1 远程服务的暴露机制
首先,框架进行服务暴露的入口在ServiceConfig
这个类中,无论XML还是注解,都会转换成ServiceBean
类,它继承了ServiceConfig
类。在服务暴露前会按照上文的配置覆盖策略进行配置:
- 遍历服务的所有方法,若没有值则尝试从-D选项中读取。
- 若还没有值,则自动从配置文件
dubbo.properties
中读取。
1.若配置的服务同时注册了多个注册中心,那么调用ServiceConfig.doExportUrls()
方法进行依次暴露:
private void doExportUrls() {
// 1.获得服务仓库。ServiceRepository是存储了所有服务端发布的服务、客户端需要访问的服务,
// 通过ServiceRepository可以获取所有Dubbo实例发布的服务和引用的服务。
// 其中有三种较为重要的属性:services、consumers、providers,类型为ConcurrentMap<String, ServiceDescriptor>
ServiceRepository repository = ApplicationModel.getServiceRepository();
// 2.注册当前所需发布的服务到服务仓库中
// 通过服务接口的Class对象,获取ServiceDescriptor,包含的数据有:接口名,服务接口每个方法的名字、入参类型、返回值类型等详细信息
ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
// 3.注册当前提供者到服务仓库中
// 该方法会该方法会创建ProviderModel对象,并将其注册到ServiceRepository中。
// 之后,dubbo可以访问该providers属性,获取所有服务端发布的服务信息。
repository.registerProvider(
getUniqueServiceName(),
ref,
serviceDescriptor,
this,
serviceMetadata
);
// 4.获取服务注册与发现的服务URL列表,返回类型是List说明Dubbo支持多协议。
List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
// 5.遍历每个协议,进行服务发布
for (ProtocolConfig protocolConfig : protocols) {
// 6.构建pathKey,格式:服务名称/group/version
String pathKey = URL.buildKey(getContextPath(protocolConfig)
.map(p -> p + "/" + path)
.orElse(path), group, version);
// 7.注册解析好的服务到服务仓库中,路径为上述新的一个映射地址pathKey
repository.registerService(pathKey, interfaceClass);
// 8.设置当前服务的元数据的服务key
serviceMetadata.setServiceKey(pathKey);
// 9.发布当前协议的服务到服务的注册与发现中心
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
2.紧接着来看看具体的发布协议的方法doExportUrlsFor1Protocol(protocolConfig, registryURLs)
:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
// 1.获取协议的名称,若没有配置,默认为dubbo协议
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}
// 2.创建一个map用来存参数,即服务的配置信息
Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, PROVIDER_SIDE);
// 3.添加运行时的参数于map中,比如服务的进程id,dubbo版本信息等
ServiceConfig.appendRuntimeParameters(map);
AbstractConfig.appendParameters(map, getMetrics());
// 。。。代码省略,都是参数的添加
MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
// 4.如果配置了元数据存储服务地址的话将元数据存储在远程,不存储在本地。
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
// 。。代码省略,添加一些泛化配置到map中
// 5.如果需要使用token来验证当前服务,就将其添加到map中
if (ConfigUtils.isEmpty(token) && provider != null) {
token = provider.getToken();
}
// 。。代码省略,对token的一些处理
// 6.将装有所有配置项的map里的数据全部添加到元数据中
serviceMetadata.getAttachments().putAll(map);
// 7.获取当前服务的IP、监听的端口等信息,并将这些信息和参数map整合成一个请求URL
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
// 8.配置扩展,支持自定义工厂来对配置项进行拦截修改
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
// 9.获取当前发布服务所配置的scope属性值,表示当前服务发布的返回,是远程还是本地
String scope = url.getParameter(SCOPE_KEY);
// 如果scope 配置为"none" 将不会发布当前服务
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
// 10.如果没有配置为"none" 或者"remote" ,那就发布到本地,若不配置也会发布到本地。
// 10.此时发布到本的话会将协议改为"injvm", ip改成"127.0.0.1", 端口改为"0",然后再发布
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 10.进行本地服务的暴露
exportLocal(url);
}
// 11.若发布的类型是remote,则将服务信息发布到远程服务中心
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
// 12.循环所有处理过的注册中心地址进行发布
for (URL registryURL : registryURLs) {
//如果需要发布的服务协议是injvm就不发布。
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
continue;
}
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
// 加载dubbo monitor的URL实例
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
if (monitorUrl != null) {
// 13.如果配置了监控地址,那么服务调用信息会进行上报
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
// ...省略
// 14.获取当前dubbo服务URL中的proxy参数
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
// 15.使用ProxyFactory通过动态代理来转换成Invoker
// 将服务实例ref转换成Invoker,registryURL存储的是注册中心地址,使用export作为key追加服务元数据信息
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 16.服务暴露后向注册中心注册服务信息
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
// ...省略代码
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 17.若没有注册中心,则直接暴露服务
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
MetadataUtils.publishServiceDefinition(url);
}
}
this.urls.add(url);
}
3.可以发现,最后都是通过Exporter来进行服务的暴露的,前面基本上是对一些参数的配置以及是做出判断(进行本地暴露还是远程暴露),在将服务实例ref转换成Invoker之后,如果有注册中心时则进行更细粒度的控制,比如先进行服务暴露再注册服务元数据。那么接下来看下其具体实现PROTOCOL.export(wrapperInvoker)
:
// 最终调用的是RegistryProtocol.export()方法
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 1.获取注册中心地址
URL registryUrl = getRegistryUrl(originInvoker);
// 2.获取当前接口提供者的地址
URL providerUrl = getProviderUrl(originInvoker);
// 3.获取订阅的URL
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 4.创建一个覆盖订阅的监听器,当服务发布完成后,会将上述获取到的订阅的URL进行覆盖。
// 意义就是当服务重新发布后需要修改注册中心的据,即进行数据的更新
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 5.添加service配置覆盖监听器 以及提供者配置信息覆盖监听器
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
// 6.发布服务并返回一个发布器的包装类实例,包含Invoker的销毁方法
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// 7.根据注册中心来获取对应的注册器,如Zookeeper、Nacos。 以及获取需要注册到注册中心的提供者的服务地址
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// 8.如果需要注册那就将服务提供者的地址注册到注册中心,
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) {
// doLocalExport主要是服务暴露并绑定一个Netty连接,到这里紧接着:连接注册中心进行服务提供者注册
// 而相关的register()代码可以看我的另外一篇博客,Dubbo原理二-Zookeeper的,里面有将Zookeeper注册的具体实现
// 而这里走的就是dubbo-Zookeeper的一个注册流程(注册中心以Zookeeper为例)
register(registryUrl, registeredProviderUrl);
}
// 9.将已经注册的服务添加到provider model提供者模型中
registerStatedUrl(registryUrl, registeredProviderUrl, register);
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
// 10.通知服务注册中心进行当前发布的服务的数据进行覆盖,用于处理动态配置
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// 11.通知当前的Registry协议的监听器
notifyExport(exporter);
// 12.服务销毁收尾工作,比如关闭端口、反注册服务信息等。
return new DestroyableExporter<>(exporter);
}
4.再来看下上一个步骤中的第六小步:发布服务并返回一个发布器的包装类实例,doLocalExport(originInvoker, providerUrl)
方法:
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
String key = getCacheKey(originInvoker);
// 最终会来到DubboProtocol的export方法
return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
});
}
public class DubboProtocol extends AbstractProtocol {
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
// 1.根据服务的分组、版本、接口和端口来构造一个key
String key = serviceKey(url);
// 2.把exporter存储到单例map中进行缓存
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter);
// ....
// 务初次暴露会创建监听服务器
openServer(url);
optimizeSerialization(url);
return exporter;
}
private void openServer(URL url) {
//。。
serverMap.put(key, createServer(url));
//。。
}
private ProtocolServer createServer(URL url) {
// 1.配置一些服务特性
url = URLBuilder.from(url)
// send readonly event when server closes, it's enabled by default
.addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
// enable heartbeat by default
.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
.addParameter(CODEC_KEY, DubboCodec.NAME)
.build();
// 2.从提供者的地址中获取服务的类型,没有配置的话,默认是netty服务类型
String str = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);
//...
try {
// 3.创建创建NettyServer并且初始化Handler,主要是绑定服务提供者的线程,搭建一个TCP连接(和注册中心)
server = Exchangers.bind(url, requestHandler);
//...
str = url.getParameter(CLIENT_KEY);
if (str != null && str.length() > 0) {
Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
if (!supportedTypes.contains(str)) {
throw new RpcException("Unsupported client type: " + str);
}
}
return new DubboProtocolServer(server);
}
}
1.2 本地服务的暴露机制
本地服务的暴露相对而言比较简单(上文也有提及本地服务的暴露,可以看第二大步的注释10),从ServiceConfig下的exportLocal方法出发:
private void exportLocal(URL url) {
// 显式的指定injvm协议进行暴露,协议改为"injvm", ip改成"127.0.0.1", 端口改为"0"
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
// 调用 InjvmProtocol的export方法,主要是把服务保存在内存中
Exporter<?> exporter = PROTOCOL.export(
PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
本地暴露的export主要用的是InjvmProtocol的export方法,实现非常简单:
public class InjvmProtocol extends AbstractProtocol implements Protocol {
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
// 直接返回 InjvmExporter实例对象
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
// 构造函数内部会把当前Invoker加入exporterMap,即本地缓存
InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
super(invoker);
this.key = key;
this.exporterMap = exporterMap;
exporterMap.put(key, this);
}
}
到这里,肯定是需要进行一个大总结的,否则内容太多看起来也不直观
1.3 Dubbo服务暴露原理总结
对于远程服务暴露,流程如下:
- 服务暴露前,根据上下文的配置覆盖策略进行配置。
- 服务暴露入口于
ServciceConfig.doExportUrls()
方法,主要做这么几件事:
2.1 获得服务仓库(存储了所有服务端发布的服务、客户端需要访问的服务)。
2.2 注册当前所需发布的服务(即接口)和服务提供者到服务仓库中。
2.3 获取服务注册与发现的服务URL列表
2.4 遍历每个协议,进行服务发布,根据服务的名称+group+版本号进行服务URL映射,并将其作为元数据的服务key。
2.5 发布当前协议的服务到服务的注册与发现中心,调用doExportUrlsFor1Protocol()方法。
doExportUrlsFor1Protocol()
方法中,主要做这么几件事:
3.1 获取协议名称,并且创建一个用于存储配置参数的集合map。
3.2 将配置信息、运行时配置、泛化配置、验证信息等加入到map中。
3.3 将当前服务的IP、监听的端口和参数map整合成一个请求URL。
3.4 进行配置的扩展。
3.5 获取当前发布服务所配置的scope属性值,判断当前服务发布返回的是远程还是本地。
----3.5.1 若 scope的值是"none" 或者不是"remote" ,那就发布到本地,请看下面的本地服务暴露流程。
----3.5.2 否则进行远程发布,看3.6↓
3.6 循环所有处理过的注册中心地址进行发布,获取当前dubbo服务URL中的proxy
参数
3.7 将服务实例ref通过动态代理的方式转换成Invoker,(包含Exporter)。
3.8 服务暴露后通过Exporter向注册中心注册服务信息,调用PROTOCOL.export(wrapperInvoker)方法
- 对于
export()
方法,远程暴露先调用的是RegistryProtocol.export()
方法,主要做五件事(具体看第3、4步中的代码注释):
4.1 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。
4.2 创建注册中心对象,与注册中心创建TCP连接(Netty实现)。
4.3 注册服务元数据到注册中心。
4.4 订阅configurators节点,监听服务动态属性变更事件(通过覆盖订阅的监听器来实现,进行数据覆盖更新)。
4.5 服务销毁收尾工作,比如关闭端口、反注册服务信息等(通过发布器的包装类实例来实现,包含Invoker的销毁方法)。
其实上面还有很多步骤都省略了(都在注释里),对总结在进行精简可得流程如下:
- 进行参数配置,然后根据每个服务进行服务暴露。
- 将每个服务ref通过动态代理的方式转换成一个Invoker,再通过Invoker来获得
Exporter
对象。 Exporter
进行服务的具体暴露,会通过Netty(默认实现)来和注册中心搭建一个TCP连接,负责监听数据的更新并且进行数据覆盖。- 暴露完成后,再进行元数据的注册(就是往Zookeeper上创建一个节点)。
- 进行服务销毁的收尾工作。
对于本地服务暴露,流程如下:
- 显式的指定injvm协议进行暴露,协议改为
"injvm",
IP改成"127.0.0.1"
, 端口改为"0"
。 - 通过返回
InjvmExporter
实例对象,将当前Invoker加入exporterMap
,进行本地的缓存。
二. Dubbo服务消费原理解析
首先,同样的来看下RPC框架的一个服务消费原理图:
整体来看,Dubbo做服务消费也分成两个部分:
- 通过持有的远程服务实例来生成Invoker,即远程代理对象。
- 把Invoker通过动态代理转换成实现用户接口的动态代理引用。
服务消费的入口在ReferenceBean
类下的getObject
方法中,不管是XML形式还是注解形式,最终都会转换成一个ReferenceBean
类,它继承了ReferenceConfig
类。并且在服务消费前同样按照文章最开始的覆盖策略进行配置。(该过程形似服务暴露)
1.先来看下ReferenceBean
下的getObject()
方法:
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean,
ApplicationContextAware, InitializingBean, DisposableBean {
@Override
public Object getObject() {
return get();
}
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
if (ref == null) {
// 如果需要注入的dubbo引用为空,那就初始化后再返回ref
init();
}
return ref;
}
// 紧接着来看下init()方法:
public synchronized void init() {
// 代码省略。。即检查配置和进行配置的加载
// 1.使用存放配置参数的map去创建一个代理类,然后赋值给ref属性。
ref = createProxy(map);
// 代码省略。。
// 2.修改初始化变量为true:代表当前的服务引用以及初始化完成
initialized = true;
checkInvokerAvailable();
dispatch(new ReferenceConfigInitializedEvent(this, invoker));
}
}
2.可得最终通过createProxy(map)
方法来获得一个代理类:
private T createProxy(Map<String, String> map) {
// 1.若需要发布injvm协议则发布
if (shouldJvmRefer(map)) {
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
invoker = REF_PROTOCOL.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
urls.clear();
// 2.如果是直连,那就进行url处理。
if (url != null && url.length() > 0) {
// 。。URL的地址处理
} else {
// 3.如果不需要发布injvm协议,那就载入注册中心的地址
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
// 。。添加集合MAP、URL中的参数
}
}
// 4.如果是单注册中心那就使用注册中心的地址来进行引用,会使用到一个自适应扩展点Protocol
// 此时由于url是注册中心的地址,因此这里的Invoker是RegistryProtocol实例。
if (urls.size() == 1) {
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
} else {
// 5.如果是多注册中心那就进行循环引用,逐个获取服务并添加到invokers列表中
// 通过Cluster将多个Invoker转换成一个Invoker
}
}
// 6.将服务的元数据进行保存,默认会写入到本地。
URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map);
MetadataUtils.publishServiceDefinition(consumerURL);
// 7.使用服务引用生成的invoker来构建一个代理类(默认是使用javassist技术来构建)
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
总结就是:
- 前期就是根据配置的参数来决定是直连消费、单/多注册中心消费,再对URL进行对应的处理。
- 调用
Protocol
的refer()
方法获得引用Invoker。 - 再通过Invoker生成一个代理类。
3.紧接着来看下Protocol
的refer()
方法:type是接口类型,URL是载入后的注册中心地址。 Protocol
是一个自适应扩展点,此时的自适应扩展其实就是被包装的RegistryProtocol
实现
因此来看下RegistryProtocol
类的refer
方法,主要是触发数据的拉取、订阅和服务Invoker转换等操作:
@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = getRegistryUrl(url);
// 1.根据具体注册中心协议(如Zookeeper)来创建一个具体注册中心的实例
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// 2.根据配置来处理多分组结果聚合
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url, qs);
}
}
// 获取集群容错策略
Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
// 3.处理订阅数据,并通过Cluster合并多个Invoker
return doRefer(cluster, registry, type, url, qs);
}
4.从doRefer(cluster, registry, type, url, qs)
方法出发:
public class RegistryProtocol implements Protocol {
protected <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url, Map<String, String> parameters) {
// 获得当前消费者的URL
URL consumerUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
ClusterInvoker<T> migrationInvoker = getMigrationInvoker(this, cluster, registry, type, url, consumerUrl);
// 构造拦截器链
return interceptInvoker(migrationInvoker, url, consumerUrl);
}
protected <T> Invoker<T> interceptInvoker(ClusterInvoker<T> invoker, URL url, URL consumerUrl) {
// 获得所有注册协议的相关监听器
List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) {
return invoker;
}
// 根据监听器的类型来进行监听器和引用的绑定
for (RegistryProtocolListener listener : listeners) {
listener.onRefer(this, invoker, consumerUrl);
}
return invoker;
}
}
listener.onRefer()
方法最终会执行到RegistryProtocol
的doCreateInvoker()
方法:
protected <T> ClusterInvoker<T> doCreateInvoker(DynamicDirectory<T> directory, Cluster cluster, Registry registry, Class<T> type) {
// 设置目录的注册器和协议
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 构建一个消费地址例
Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
URL urlToRegistry = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
// 当前的消费者是否需要注册,判断标准是消费端的register配置,默认是需要注册
if (directory.isShouldRegister()) {
directory.setRegisteredConsumerUrl(urlToRegistry);
// 将消费者地址注册到注册中心,如果是zookeeper的话就会进行消费者地址写入
registry.register(directory.getRegisteredConsumerUrl());
}
// 设置服务路由链,根据配置进行设置
directory.buildRouterChain(urlToRegistry);
// 订阅当前服务的提供者
directory.subscribe(toSubscribeUrl(urlToRegistry));
// 将当前的服务目录加入到集群中并返回一个Invoker
return (ClusterInvoker<T>) cluster.join(directory);
}
refer()
方法如何获得一个Invoker?总结就是:
- 通过
RegistryProtocol.refer()
方法实现数据的拉取、订阅和服务Invoker转换等操作。 - 其中根据具体的注册中心协议来创建一个注册中心实例,并且根据容错策略来合并多个Invoker。(可能有多个服务)
- 构建当前的消费者URL,并以此获取所有相关的监听器,并根据监听器去进行服务引用的注入。
- 对于每一个不同的目录,设置基本的服务路由链,并进行当前服务的订阅,并将目录合并成一个集群Cluster中。
- 最终返回一个Invoker。
目录:RegistryDirectory,当前接口的服务提供者的目录,里面会维护Invoker列表,每一个invoker就是一个服务提供者节点封装,在订阅的时候根据服务提供者地址上的注册器,转换成每一个Invoker实例存directory中。
那么最后,将得到的invoker通过动态代理的方式来构建一个代理类(默认是使用javassist技术来构建),最终的方法调用则是通过代理类来执行invoke方法。
Dubbo服务消费原理总结
总结:
- 服务消费的入口在ReferenceBean类下的getObject方法中,主要负责dubbo引用
ref
进行初始化,即通过配置参数去创建一个代理类,然后赋值给ref属性。 - 调用方法Protocol.refer(),根据接口的类型、载入的注册中心地址进行ref的注入。Protocol是一个自适应扩展点,此时的自适应扩展其实就是被包装的
RegistryProtocol
实现,即调用RegistryProtocol.refer()方法。 - 其中会根据协议类型来创建对应的注册中心实例,并根据容错策略来合并Invoker。
- 对于合并的每一个Invoker,通过监听器去进行具体的实现,如设置基本的服务路由链,进行当前服务的订阅等操作。最终合并到Cluster集群中然后返回。
- 将服务的元数据进行保存,默认会写入到本地。
- 使用服务引用生成的invoker来构建一个代理类。(默认是使用javassist技术来构建)。
精简总结可得:
- Dubbo要想调用一个服务,需要对对应服务进行引用ref的初始化。
- 初始化则根据接口类型、URL地址的不同来进行属性注入(不同的Invoker)。
- 每个Invoker又会进行对应的服务注册和订阅,并将对应服务的元数据进行保存(默认写入本地)。
- Dubbo会根据容错策略将多个Invoker(若服务同时配置多个注册中心)合并成一个并返回。
- 通过动态代理的方式,通过Invoker返回引用ref,完成初始化。
写的感觉依旧不是很好,若文章写的有问题,还望指出!