Dubbo 3.x源码(13)—Dubbo服务发布导出源码(2)

基于Dubbo 3.1,详细介绍了Dubbo服务的发布与引用的源码。

此前我们学习了Dubbo 3.x源码(12)—Dubbo服务发布导出源码(1),也就是Dubbo服务发布导出的入口源码,现在我们继续学习,服务导出的核心方法doExportUrls的源码。

  1. Dubbo 3.x源码(11)—Dubbo服务的发布与引用的入口
  2. Dubbo 3.x源码(12)—Dubbo服务发布导出源码(1)
  3. Dubbo 3.x源码(13)—Dubbo服务发布导出源码(2)

文章目录

  • 1 doExportUrls导出全部服务url
  • 2 registerService注册服务
    • 2.1 ReflectionServiceDescriptor反射的服务描述符
  • 3 loadRegistries加载注册中心地址
    • 3.1 genCompatibleRegistries获取兼容的双注册地址
  • 4 doExportUrlsFor1Protocol单协议多注册中心导出
  • 5 exportUrl导出服务url
    • 5.1 exportLocal本地导出
    • 5.2 exportRemote远程导出
    • 5.3 publishServiceDefinition发布服务元数据
  • 6 总结

1 doExportUrls导出全部服务url

该方法根据配置不同的协议,导出服务地址到多个注册中心,Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议。

大概步骤为:

  1. 调用registerService方法,注册serviceDescriptor服务描述符到本地内存的服务仓库ModuleServiceRepository的services缓存中,即注册service
  2. 构建此服务对应的服务提供者模型providerModel,并且调用registerProvider方法注册到本地内存的服务仓库ModuleServiceRepository的providers缓存中,即注册provider
  3. 通过loadRegistries方法加载全部的注册中心url地址信息,方便后续对于所有的注册中心进行服务导出。
  4. 遍历当前service支持的所有协议,对每一个协议调用doExportUrlsFor1Protocol方法尝试向多个注册中心url集合进行服务导出
/*** ServiceConfig的方法* <p>* 导出服务url*/
@SuppressWarnings({"unchecked", "rawtypes"})
private void doExportUrls() {//获取Module级别的服务存储仓库,其内部保存着服务提供者的缓存ModuleServiceRepository repository = getScopeModel().getServiceRepository();//服务描述符,通过它可以获取服务描述信息,例如服务提供的方法,服务接口名,服务接口Class等ServiceDescriptor serviceDescriptor;//服务实现类型是否属于ServerService类型,一般都不属于final boolean serverService = ref instanceof ServerService;/** 1 注册serviceDescriptor服务描述符到本地内存的服务仓库ModuleServiceRepository的services缓存中,即注册service*///如果属于ServerServiceif (serverService) {//通过服务实现获取服务描述符serviceDescriptor = ((ServerService) ref).getServiceDescriptor();//注册服务描述符到服务仓库内部的services集合中repository.registerService(serviceDescriptor);} else {//大部分情况的逻辑//注册服务描述符到服务仓库内部的services集合中serviceDescriptor = repository.registerService(getInterfaceClass());}/** 2 构建此服务对应的服务提供者模型providerModel,并且注册到本地内存的服务仓库ModuleServiceRepository的providers缓存中,即注册provider*/providerModel = new ProviderModel(serviceMetadata.getServiceKey(),//服务实现ref,//服务描述符serviceDescriptor,//域模型getScopeModel(),//服务元数据,服务接口类加载器serviceMetadata, interfaceClassLoader);// Compatible with dependencies on ServiceModel#getServiceConfig(), and will be removed in a future version//与ServiceModel#getServiceConfig()上的依赖项兼容,并将在未来的版本中删除providerModel.setConfig(this);providerModel.setDestroyRunner(getDestroyRunner());//注册服务提供者模型到服务仓库内部的providers集合中repository.registerProvider(providerModel);/** 3 加载全部的注册中心url地址信息,方便后续对于所有的注册中心进行服务导出*/List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);/** 4 遍历当前service支持的协议,对每一个协议尝试向多个注册中心url集合进行服务导出*/for (ProtocolConfig protocolConfig : protocols) {//获取另一个路径keyString pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);// stub service will use generated service name//如果服务实现类型不属于ServerService类型,一般都不属于if (!serverService) {// In case user specified path, register service one more time to map it to path.//如果用户指定了路径,则再注册一次服务以将其映射到路径。repository.registerService(pathKey, interfaceClass);}/** 根据协议向多个注册中心url集合进行服务导出*/doExportUrlsFor1Protocol(protocolConfig, registryURLs);}//设置引用服务的urlproviderModel.setServiceUrls(urls);
}

2 registerService注册服务

该方法注册服务描述符到本地内存的服务仓库ModuleServiceRepository内部的services集合中。ServiceDescriptor服务描述符,通过它的相关方法可以直接获取服务描述信息,例如服务提供的方法,服务接口名,服务接口Class等。

实际上,在ApplicationModel#initialize方法中,就会创建一个服务ServiceRepository,其构造器内部会调用ModuleServiceRepository#registerService方法,在那时就会进行服务的注册了。
image.png
registerService方法的大概步骤为:

  1. 基于服务接口class构建一个ReflectionServiceDescriptor对象。
  2. 调用另一个两个参数的registerService方法完成注册,key为服务接口名,value就是对应的方法描述符
/*** ModuleServiceRepository的方法* <p>* 注册服务描述符到服务仓库内部的services集合中** @param interfaceClazz 服务接口* @return 服务描述符*/
public ServiceDescriptor registerService(Class<?> interfaceClazz) {//构建一个ReflectionServiceDescriptor对象ServiceDescriptor serviceDescriptor = new ReflectionServiceDescriptor(interfaceClazz);//调用另一个两个参数的registerService方法完成注册return registerService(interfaceClazz, serviceDescriptor);
}/*** 注册服务** @param interfaceClazz    服务接口* @param serviceDescriptor 服务描述符* @return 服务描述符*/
public ServiceDescriptor registerService(Class<?> interfaceClazz, ServiceDescriptor serviceDescriptor) {//如果该服务不存在,则存入一个空的CopyOnWriteArrayList到services并返回,否则返回此前存在的serviceDescriptors集合List<ServiceDescriptor> serviceDescriptors = services.computeIfAbsent(interfaceClazz.getName(),k -> new CopyOnWriteArrayList<>());//加锁synchronized (serviceDescriptors) {//遍历此前的serviceDescriptors集合,获取第一个interfaceClazz为当前参数的数据Optional<ServiceDescriptor> previous = serviceDescriptors.stream().filter(s -> s.getServiceInterfaceClass().equals(interfaceClazz)).findFirst();//如果存在服务if (previous.isPresent()) {//获取此前的服务描述符return previous.get();}//如果不存在服务else {//将当前服务描述符加入到集合中serviceDescriptors.add(serviceDescriptor);//返回当前服务描述符return serviceDescriptor;}}
}

2.1 ReflectionServiceDescriptor反射的服务描述符

下面看看ReflectionServiceDescriptor的构造器的源码。

serviceInterfaceClass就是服务接口class,interfaceName就是接口全路径名,随后调用initMethods方法,获取接口全部的公共方法封装为ReflectionMethodDescriptor方法描述符对象,随后存入methods缓存map集合,key为方法名,value为方法描述符集合,因为可能有同名方法,随后还会构建descToMethods缓存map集合,key为方法名,value为一个方法参数到方法描述符的映射。

public ReflectionServiceDescriptor(Class<?> interfaceClass) {
//服务接口Classthis.serviceInterfaceClass = interfaceClass;//接口全路径名this.interfaceName = interfaceClass.getName();//初始化方法描述符initMethods();
}private void initMethods() {//获取接口的所有public方法,包括自身的和从父类、接口继承的。Method[] methodsToExport = this.serviceInterfaceClass.getMethods();for (Method method : methodsToExport) {//设置访问标记method.setAccessible(true);//构建方法描述符对象MethodDescriptor methodDescriptor = new ReflectionMethodDescriptor(method);//存入methods缓存map集合,key为方法名,value为方法描述符集合,因为可能有同名方法List<MethodDescriptor> methodModels = methods.computeIfAbsent(method.getName(), (k) -> new ArrayList<>(1));methodModels.add(methodDescriptor);}//存入descToMethods缓存map集合,key为方法名,value为一个方法参数到方法描述符的映射methods.forEach((methodName, methodList) -> {Map<String, MethodDescriptor> descMap = descToMethods.computeIfAbsent(methodName, k -> new HashMap<>());methodList.forEach(methodModel -> descMap.put(methodModel.getParamDesc(), methodModel));});
}

3 loadRegistries加载注册中心地址

一个服务可以支持多个注册中心,例如zookeeper、nacos。该方法加载全部的注册中心url地址信息,方便后续对于所有的注册中心进行服务导出。大概逻辑为:

  1. 在该方法中首先获取全部的注册中心配置集合,然后对每个注册中心的配置拼接好注册中心url
  2. 随后会对url根据协议类型进行一次统一的协议替换,如果协议参数中有registry-type=service的参数,那么协议改为“service-discovery-registry”,即服务级别发现注册中心地址,否则协议取url的registry-protocol-type参数的值,如果也没有该参数,那么协议改为“registry”,即接口级别发现注册中心地址
  3. 最后调用genCompatibleRegistries方法获取兼容的注册中心地址。
/*** ConfigValidationUtils的方法* 加载全部的注册中心url地址信息** @param interfaceConfig 当前服务配置/引用配置* @param provider        是否是服务提供者* @return*/
public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {// 必要时检查&&覆盖List<URL> registryList = new ArrayList<>();ApplicationConfig application = interfaceConfig.getApplication();//获取全部的注册中心配置信息List<RegistryConfig> registries = interfaceConfig.getRegistries();if (CollectionUtils.isNotEmpty(registries)) {//遍历全部注册中心配置for (RegistryConfig config : registries) {// try to refresh registry in case it is set directly by user using config.setRegistries()//尝试刷新注册表,以防它是由用户使用config.setRegistries()直接设置的。if (!config.isRefreshed()) {config.refresh();}//拼接注册中心url地址String address = config.getAddress();if (StringUtils.isEmpty(address)) {address = ANYHOST_VALUE;}if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {//url参数mapMap<String, String> map = new HashMap<String, String>();AbstractConfig.appendParameters(map, application);AbstractConfig.appendParameters(map, config);map.put(PATH_KEY, RegistryService.class.getName());AbstractInterfaceConfig.appendRuntimeParameters(map);if (!map.containsKey(PROTOCOL_KEY)) {map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);}//根据模式 \s*[|;]+\s* 拆分urlList<URL> urls = UrlUtils.parseURLs(address, map);for (URL url : urls) {//重写urlurl = URLBuilder.from(url).addParameter(REGISTRY_KEY, url.getProtocol())//重设协议,如果协议参数中有registry-type=service的参数,那么协议改为“service-discovery-registry”,即服务级别发现注册中心地址//否则协议取url的registry-protocol-type参数的值,如果也没有该参数,那么协议改为“registry”,即接口级别发现注册中心地址.setProtocol(extractRegistryType(url)).setScopeModel(interfaceConfig.getScopeModel()).build();//provider延迟注册状态将在RegistryProtocol#export中检查  provider delay register state will be checked in RegistryProtocol#exportif (provider || url.getParameter(SUBSCRIBE_KEY, true)) {registryList.add(url);}}}}}//获取兼容的注册中心地址return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);
}

3.1 genCompatibleRegistries获取兼容的双注册地址

在该方法调用之前,会根据url参数中的registry-type参数更改协议,如果协议参数中有registry-type=service的参数,那么协议改为“service-discovery-registry”,即服务级别发现注册中心地址,否则协议取url的registry-protocol-type参数的值,如果也没有该参数,那么协议改为“registry”,即接口级别发现注册中心地址

大部分情况下,我们是不会主动在注册中心url中添加registry-type参数的,因此,一般协议都会被更改为registry。而在genCompatibleRegistries方法中,会进行一系列判断并仍然可能多添加一个service-discovery-registry协议地址,即最终返回两个协议地址,一个是接口级别的,另一个是服务级别的。这就是所谓的Dubbo3.0的服务端自动开启接口级 + 应用级双注册功能:https://dubbo.apache.org/zh/docs3-v2/java-sdk/upgrades-and-compatibility/service-discovery/service-discovery-samples/

该方法的大概逻辑为:

  1. 如果当前为服务提供者,那么需要对参数协议地址进行兼容性改造:
    1. 如果参数url协议已经是服务级别发现注册中心地址协议,即service-discovery-registry,一般都是registry,因为这需要手动配置registry-type=service参数。
      1. 获取服务注册模式registerMode:获取url参数中的register-mode属性,如果没有则默认值为dubbo.application.register-mode属性的值,该属性默认值为instance。
      2. 如果值为null,或者不是interface、instance、all三者中的一个,那么registerMode默认instance,即应用级模式。
      3. 将参数中的应用级url加入结果集。
      4. 如果服务注册模式为all,并且没有接口级地址,那么新增一个接口级地址,即registry开头,随后将接口级别注册地址加入结果集,这样配置的一个注册中心就有两个url地址了。
    2. 协议如果不是服务级别发现注册中心地址协议,即不是service-discovery-registry,一般都是走这个逻辑。
      1. 获取服务注册模式registerMode:获取url参数中的register-mode属性,如果没有则默认值为dubbo.application.register-mode属性的值,该属性默认值为all
      2. 如果值为null,或者不是interface、instance、all三者中的一个,那么registerMode默认interface,即接口级模式。
      3. 如果服务注册模式为all或者instance,并且没有应用级地址,那么新增一个应用级地址,即service-discovery-registry开头,随后将接口级别注册地址加入结果集。
      4. 如果服务注册模式为all或者interface,那么再将参数中的接口级url加入结果集。
  2. 如果当前不是服务提供者,那么直接返回参数协议地址即可。

通过上面的步骤可知,默认情况下,在Dubbo3.1中,将会对于一个注册中心返回两个注册协议地址,一个接口级别的协议地址,以registry开头,兼容Dubbo2,另一个是应用级别的协议地址,以service-discovery-registry开头,满足Dubbo3。

下面举一个这两种url地址的例子:

  1. 服务级别发现注册中心地址:service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registry1&application=demo-provider&application.version=1&dubbo=2.0.2&pid=28659&registry=zookeeper&registry-type=service&registry.type=service&timeout=20001&timestamp=1666161869858
  2. 接口级别发现注册中心地址:registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registry1&application=demo-provider&application.version=1&dubbo=2.0.2&pid=30594&registry=zookeeper&timeout=20001&timestamp=1666162612790
/*** ConfigValidationUtils的方法* 获取兼容的注册中心地址** @param scopeModel   域模型* @param registryList 注册地址* @param provider     是否是服务提供者* @return 兼容的注册中心地址*/
private static List<URL> genCompatibleRegistries(ScopeModel scopeModel, List<URL> registryList, boolean provider) {List<URL> result = new ArrayList<>(registryList.size());registryList.forEach(registryURL -> {//如果是服务提供者if (provider) {// for registries enabled service discovery, automatically register interface compatible addresses.//对于启用服务发现的注册中心,自动注册接口兼容地址。String registerMode;//协议如果是服务级别发现注册中心地址协议,即service-discovery-registry,一般都是registryif (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {//获取服务注册模式,获取url参数中的register-mode属性,默认值为dubbo.application.register-mode属性的值,该属性默认值为instanceregisterMode = registryURL.getParameter(REGISTER_MODE_KEY, ConfigurationUtils.getCachedDynamicProperty(scopeModel, DUBBO_REGISTER_MODE_DEFAULT_KEY, DEFAULT_REGISTER_MODE_INSTANCE));//如果值为null,或者不是interface、instance、all三者中的一个,那么registerMode默认instance,即应用级模式if (!isValidRegisterMode(registerMode)) {registerMode = DEFAULT_REGISTER_MODE_INSTANCE;}//加入结果result.add(registryURL);//如果服务注册模式为all,并且没有接口级地址,那么新增一个接口级地址,即registry开头if (DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)&& registryNotExists(registryURL, registryList, REGISTRY_PROTOCOL)) {URL interfaceCompatibleRegistryURL = URLBuilder.from(registryURL).setProtocol(REGISTRY_PROTOCOL).removeParameter(REGISTRY_TYPE_KEY).build();//将接口级别注册地址加入结果集result.add(interfaceCompatibleRegistryURL);}}//协议如果不是服务级别发现注册中心地址协议,即不是service-discovery-registry,一般都是registryelse {//获取服务注册模式,获取url参数中的register-mode属性,默认值为dubbo.application.register-mode属性的值,该属性默认值为allregisterMode = registryURL.getParameter(REGISTER_MODE_KEY, ConfigurationUtils.getCachedDynamicProperty(scopeModel, DUBBO_REGISTER_MODE_DEFAULT_KEY, DEFAULT_REGISTER_MODE_ALL));//如果值为null,或者不是interface、instance、all三者中的一个,那么registerMode默认interface,即接口级模式if (!isValidRegisterMode(registerMode)) {registerMode = DEFAULT_REGISTER_MODE_INTERFACE;}//如果服务注册模式为all或者instance,并且没有应用级地址,那么新增一个应用级地址,即service-discovery-registry开头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);}//如果服务注册模式为all或者interfaceif (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) {//将接口级别注册地址加入结果集result.add(registryURL);}}//注册状态报告服务FrameworkStatusReportService reportService = ScopeModelUtil.getApplicationModel(scopeModel).getBeanFactory().getBean(FrameworkStatusReportService.class);//注册报告服务reportService.reportRegistrationStatus(reportService.createRegistrationReport(registerMode));}//如果不是服务提供者,那么直接将url加入结果集else {result.add(registryURL);}});return result;
}

4 doExportUrlsFor1Protocol单协议多注册中心导出

该方法根据单个协议向多个注册中心url集合进行服务导出。

  1. 首先会构建给定协议的服务导出url,在dubbo中,各种交互都是通过url的形式进行数据传递的,所以url在dubbo源码中无处不在。
    1. 这里构建的服务导出url中包含ip、端口、服务接口、服务方法等参数,样式例如:dubbo://127.0.0.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&application.version=1&background=false&bind.ip=127.0.0.1&bind.port=20880&delay=5000&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=1688&side=provider&timeout=3000&timestamp=1666274253014。
  2. 然后调用exportUrl方法对所有的注册中心进行服务url导出。
/*** ServiceConfig的方法* <p>* 根据协议向多个注册中心url集合进行服务导出** @param protocolConfig 协议* @param registryURLs   多个注册中心url集合*/
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {//构建协议导出服务的各种参数,用于组装服务导出的urlMap<String, String> map = buildAttributes(protocolConfig);// remove null key and null value//移除空key和空值的属性map.keySet().removeIf(key -> StringUtils.isEmpty(key) || StringUtils.isEmpty(map.get(key)));// init serviceMetadata attachments//将map存入服务元数据的附加数据中serviceMetadata.getAttachments().putAll(map);//通过协议 + 参数 构建服务导出url,包含ip、端口、服务接口、服务方法等参数,样式例如://dubbo://127.0.0.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&application.version=1&background=false&bind.ip=127.0.0.1&bind.port=20880&delay=5000&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=1688&side=provider&timeout=3000&timestamp=1666274253014//在dubbo中,各种交互都是通过url的形式进行数据传递的URL url = buildUrl(protocolConfig, map);/** 导出协议url*/exportUrl(url, registryURLs);
}

5 exportUrl导出服务url

该方法是导出服务url的入口方法,Dubbo在服务导出前会根据scope属性判断服务导出的范围,none代表不导出、local仅导出到本地JVM、remote会导出到远程,默认情况下会同时导出到本地JVM和远程。

通过exportLocal进行本地导出,通过exportRemote方法进行远程导出,在远程导出完毕之后,还会调用MetadataUtils#publishServiceDefinition方法发布服务元数据信息。

/*** 导出服务url** @param url          要导出的服务url* @param registryURLs 注册中心url地址*/
private void exportUrl(URL url, List<URL> registryURLs) {String scope = url.getParameter(SCOPE_KEY);// don't export when none is configured//如果没有配置scope,则不要导出if (!SCOPE_NONE.equalsIgnoreCase(scope)) {// export to local if the config is not remote (export to remote only when config is remote)//如果配置不是远程的则导出到本地(只有当配置是远程的时才导出到远程)if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {/** 1 本地导出*/exportLocal(url);}// export to remote if the config is not local (export to local only when config is local)//如果配置不是本地的,则导出到远程(仅当配置为本地时,导出到本地)if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {/** 2 远程导出*/url = exportRemote(url, registryURLs);/** 3 发布服务元数据信息*/if (!isGeneric(generic) && !getScopeModel().isInternal()) {MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());}}}//服务urls集合加入该导出的urlthis.urls.add(url);
}

5.1 exportLocal本地导出

构建injvm协议的url,ip地址固定为127.0.0.1,端口为0。可以看到所谓的本地导出,没有监听端口,没有远程调用。说白了就是本jvm的消费者调用本jvm上的服务提供者,但是仍然会走dubbo的Filter和Listener。随后调用doExportUrl方法导出服务url。

/*** ServiceConfig的方法* <p>* 本地导出服务,采用injvm协议* always export injvm*/
private void exportLocal(URL url) {//构建injvm协议的url,ip地址固定为127.0.0.1,端口为0。可以看到所谓的本地导出,没有监听端口,没有远程调用//说白了就是本jvm的消费者调用本jvm上的服务提供者,但是仍然会走dubbo的Filter和ListenerURL local = URLBuilder.from(url).setProtocol(LOCAL_PROTOCOL).setHost(LOCALHOST_VALUE).setPort(0).build();local = local.setScopeModel(getScopeModel()).setServiceModel(providerModel);/** 导出url*/doExportUrl(local, false);logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}

5.2 exportRemote远程导出

该方法会对服务url依次调用注册中心registryURL并通过doExportUrl方法进行远程服务导出,在导出前,注册中心url的attributes中添加key为export,value为服务导出url的属性。

注意,如果注册中心url集合为空,那么表示直连服务消费者,如果注册中心配置成<dubbo:registry address=“N/A”/> 表示服务消费者直连服务提供者,消费者可通过在service标签中配置url属性配置直连url。

/*** ServiceConfig的方法* <p>* 远程导出url** @param url          导出的服务url* @param registryURLs 注册中心url* @return*/
private URL exportRemote(URL url, List<URL> registryURLs) {//如果注册中心url集合不为空,那么进行远程导出if (CollectionUtils.isNotEmpty(registryURLs)) {//遍历注册中心url,依次注册for (URL registryURL : registryURLs) {//如果是应用级注册中心,那么为url添加service-name-mapping=true参数if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");}//if protocol is only injvm ,not register//如果导出的服务url为injvm,那么不会进行导出if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {continue;}//动态添加dynamic属性url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));//加载监控配置urlURL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);if (monitorUrl != null) {url = url.putAttribute(MONITOR_KEY, monitorUrl);}// For providers, this is used to enable custom proxy to generate invoker//对于服务提供者,这用于支持自定义代理生成调用程序String proxy = url.getParameter(PROXY_KEY);if (StringUtils.isNotEmpty(proxy)) {registryURL = registryURL.addParameter(PROXY_KEY, proxy);}if (logger.isInfoEnabled()) {if (url.getParameter(REGISTER_KEY, true)) {logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL.getAddress());} else {logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);}}//注册中心url的attributes中添加key为export,value为服务导出url的属性,进行服务导出doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true);}}//否则进行直接导出,者表示直连服务消费者,如果注册中心配置成<dubbo:registry address="N/A"/> 表示服务消费者直连服务提供者//消费者可通过在service标签中配置url属性配置直连urlelse {if (logger.isInfoEnabled()) {logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);}doExportUrl(url, true);}return url;
}

5.3 publishServiceDefinition发布服务元数据

在进行了远程服务导出之后,还需要通过MetadataUtils#publishServiceDefinition方法将服务的元数据信息发布到全部的元数据中心。

该方法最终是调用MetadataReport的storeProviderMetadata方法同步ProviderMetadata,调用MetadataReport的storeConsumerMetadata方法同步ConsumerMetadata,这两个方法我们在此前Dubbo启动元数据中心的时候就讲过了。

/*** 将生产者或者消费者的元数据信息发布到元数据中心** @param url               服务url* @param serviceDescriptor 服务描述符* @param applicationModel  应用程序模型*/
public static void publishServiceDefinition(URL url, ServiceDescriptor serviceDescriptor, ApplicationModel applicationModel) {if (getMetadataReports(applicationModel).size() == 0) {String msg = "Remote Metadata Report Server is not provided or unavailable, will stop registering service definition to remote center!";logger.warn(msg);return;}try {//服务类型String side = url.getSide();//如果是provider,即服务提供者if (PROVIDER_SIDE.equalsIgnoreCase(side)) {//获取服务key {group}/{servicePath}:{version}String serviceKey = url.getServiceKey();//获取全部服务元数据,宝库哦服务接口名、源代码位置、服务方法、注解等等服务描述信息FullServiceDefinition serviceDefinition = serviceDescriptor.getFullServiceDefinition(serviceKey);if (StringUtils.isNotEmpty(serviceKey) && serviceDefinition != null) {serviceDefinition.setParameters(url.getParameters());  //url的参数设置到serviceDefinition中//遍历全部元数据中心for (Map.Entry<String, MetadataReport> entry : getMetadataReports(applicationModel).entrySet()) {MetadataReport metadataReport = entry.getValue();if (!metadataReport.shouldReportDefinition()) {logger.info("Report of service definition is disabled for " + entry.getKey());continue;}//调用storeProviderMetadata方法同步ProviderMetadata,该方法我们在此前Dubbo启动元数据中心的时候就讲过了metadataReport.storeProviderMetadata(new MetadataIdentifier(url.getServiceInterface(),url.getVersion() == null ? "" : url.getVersion(),url.getGroup() == null ? "" : url.getGroup(),PROVIDER_SIDE,applicationModel.getApplicationName()), serviceDefinition);}}}//如果是服务消费者else {//遍历全部元数据中心for (Map.Entry<String, MetadataReport> entry : getMetadataReports(applicationModel).entrySet()) {MetadataReport metadataReport = entry.getValue();if (!metadataReport.shouldReportDefinition()) {logger.info("Report of service definition is disabled for " + entry.getKey());continue;}//调用storeConsumerMetadata方法同步ConsumerMetadata,该方法我们在此前Dubbo启动元数据中心的时候就讲过了metadataReport.storeConsumerMetadata(new MetadataIdentifier(url.getServiceInterface(),url.getVersion() == null ? "" : url.getVersion(),url.getGroup() == null ? "" : url.getGroup(),CONSUMER_SIDE,applicationModel.getApplicationName()),url.getParameters());}}} catch (Exception e) {//ignore errorlogger.error("publish service definition metadata error.", e);}
}

这两个方法会将服务元数据信息同步到元数据中心,zookeeper作为元数据中心时,节点路径/dubbo/{pathTag}/{servicePath}/{version}/{group}/{side},例如 /dubbo/metadata/org.apache.dubbo.demo.GreetingService/1.0.0/greeting/provider/demo-provider。

zookeeper中的provider的元数据格式如下:
image.png
zookeeper中的consumer的元数据格式如下:
image.png

6 总结

本次我们学习了服务导出的核心方法doExportUrls的部分源码。通过源码我们可以知道:

默认情况下,在Dubbo3.1中,将会对于一个注册中心返回两个注册协议地址,一个接口级别的协议地址,以registry开头,兼容Dubbo2,另一个是应用级别的协议地址,以service-discovery-registry开头,满足Dubbo3。即服务端自动开启接口级 + 应用级双注册功能:https://dubbo.apache.org/zh/docs3-v2/java-sdk/upgrades-and-compatibility/service-discovery/service-discovery-samples/。

在导出服务的时候,Dubbo一个服务可以支持多个不同的协议,例如dubbo、rmi、hessian、http、webservice、thrift、redis,以及支持多个不同的注册中心,例如zookeeper、nacos。服务将会被同时根据不同的协议想不同的注册中心进行服务导出。

doExportUrls方法中,首先构建并注册对应服务的serviceDescriptor、providerModel,然后通过loadRegistries加载全部的注册中心url地址信息,最后遍历当前服务接口支持的协议,依次调用doExportUrlsFor1Protocol方法,尝试将每个协议向多个注册中心url进行服务导出。

  1. loadRegistries方法用于加载注册中心url地址,获取全部注册中心配置集合,然后对每个注册中心的配置拼接好注册中心url,协议将被改写为注册中心协议。
    1. 在Dubbo3.1版本中,如果url没有特殊参数,那么默认情况下,每个注册中心都会生成两个注册中心协议url,一个service-discovery-registry开头的协议表示服务级别发现注册中心,一个registry开头的协议表示接口级别发现注册中心。也就是说,服务将会同时进行接口级别和服务级别的注册。
    2. doExportUrlsFor1Protocol方法中,会根据协议参数和当前服务元数据,构建出一个服务导出协议url,然后调用exportUrl方法继续导出。

exportUrl方法中,获取scope属性判断服务导出的范围,none代表不导出、local仅导出到本地JVM、remote会导出到远程,默认为null,表示会同时导出到本地JVM和远程,分别调用exportLocal和exportRemote方法。在远程导出完毕之后,还会调用MetadataUtils#publishServiceDefinition方法发布服务元数据信息。

  1. exportLocal 方法用于本地导出,用不上注册中心协议url,主要用于本jvm的消费者调用本jvm上的服务提供者,不涉及远程网络调用。
    1. 将服务导出协议url改写为injvm协议的url,ip地址固定为127.0.0.1,端口为0。可以看到所谓的本地导出,没有监听端口,没有远程调用,但是仍然会走dubbo的Filter和Listener。随后调用doExportUrl方法执行协议导出。
  2. exportRemote方法用于远程导出,涉及到服务注册、启动服务端nettyserver等逻辑,用于远程网络调用。
    1. 遍历全部注册中心协议url添加参数,例如如果是应用级注册中心,那么为url添加service-name-mapping=true参数。
    2. 在注册中心协议url内部的attributes中添加属性,key为export,value为服务导出协议url,随后调用doExportUrl方法执行协议导出。

exportLocal 和exportRemote方法最终都会调用doExportUrl方法,该方法是服务导出的核心方法。下次我们将继续深入学习导出对某一个服务忌语某一个协议导出到某一个注册中心的doExportUrl方法的具体源码。

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

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

相关文章

一文读懂Java中的设计模式——单例模式!默认情况下,Spring的Bean就是单例的

单例模式概念 单例模式确保某一个类只有一个实例&#xff0c;而且自行实例化并向整个系统提供这个实例。单例模式只应在有真正的“单一实例”的需求时才可使用。总结起来单例模式有三个优点&#xff1a; 全局访问共享资源&#xff1a;当需要在应用程序的多个地方共享和使用相…

跑代码中遇到的错误合集(持续更新)

1.TypeError: dropout(): argument ‘input‘ (position 1) must be Tensor, not str 原因&#xff1a;dropout函数接收到的参数是一个字典类型(需手动设置其不要返回字典类型) 解决步骤: 1.根据代码定位到dropout函数 2.定位到函数中的参数 3.对给dropout函数参数赋值的函数的…

[渗透测试学习] Sau - HackTheBox

首先是信息搜集&#xff0c;nmap扫一下 nmap -sV -sC -p- -v 10.10.11.224 发现存在两个端口&#xff0c;55555端口有http服务&#xff0c;访问一下 获得线索request-baskets版本为1.2.1&#xff0c;搜索发现存在漏洞 那么我们试试构造ssrf&#xff0c;create的时候bp抓包 构…

定时器TIM HAL库+cubeMX(上)

定时器时钟源APB1 36MHz 一.基本定时器 1.基本框图 2.溢出时间计算 3.配置定时器步骤 TIM_HandleTypeDef g_timx_handle;/* 定时器中断初始化函数 */ void btim_timx_int_init(uint16_t arr, uint16_t psc) {g_timx_handle.Instance TIM6;g_timx_handle.Init.Prescaler p…

【数据安全】金融行业数据安全保障措施汇总

数字化的今天&#xff0c;数据的价值不可估量&#xff0c;尤其是金融行业&#xff0c;数据不仅代表着企业的核心资产&#xff0c;还涉及到客户的隐私和信任。因此对于金融行业而言&#xff0c;保障数据安全至关重要。下面我们就来一起讨论为什么金融行业要保障数据安全&#xf…

Idea执行bat使用maven打包springboot项目成docker镜像并push到Harbor

如果执行以下命令失败&#xff0c;先把mvn的-q参数去掉&#xff0c;让错误输出到控制台。 《idea配置优化、Maven配置镜像、并行构建加速打包、解决maven打包时偶尔几个文件没权限的问题》下面的使用company-repo私有仓库和阿里云镜像仓库同时使用的配置参考。 bat echo off …

要实现长页面滑动到指定位置触发动画效果(亲测有效)

1.添加触发动画的元素&#xff1a;在你的 HTML 文件中&#xff0c;将需要触发动画的元素添加相应的类名<div class"animation"> <p>安全工矿 智能工矿 安全工矿 智能工矿</p> </div> 给一个 <div> 元素添加 .animation…

JVM 详解(JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路)

目录 JVM 详解&#xff08;JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路&#xff09;1、概念&#xff1a;什么是 JVM ?JVM 的作用&#xff1f; 2、JVM 的主要组成部分&#xff1f;类加载器&#xff08;Class Loader&#xff09;&#xff1a;简单…

02基于matlab的卡尔曼滤波

基于matlab的卡尔曼滤波&#xff0c;可更改状态转移方程&#xff0c;控制输入&#xff0c;观测方程&#xff0c;设置生成的信号的噪声标准差&#xff0c;设置状态转移方差Q和观测方差R等参数&#xff0c;程序已调通&#xff0c;需要直接拍下。

Vue学习计划-Vue2--VueCLi(五)全局事件总线、消息订阅与发布(pubsub)

抛出问题:我们多级组件&#xff0c;或者任意不想关的子组件如何传递数据呢&#xff1f; 1. 全局事件总线&#xff08;$bus&#xff09; 一种组件间通信的方式&#xff0c;适用于任意组件间通信 全局事件总线示意图&#xff1a; 安装全局事件总线&#xff1a; new Vue({..…

数据结构和算法-图的基本概念及邻接矩阵法和邻接表法和十字链表法和链表链表法

文章目录 图的概念总览图的定义图逻辑结构的应用无向图和有向图简单图和多重图顶点的度&#xff0c;入读&#xff0c;出度顶点-顶点的关系描述连通图&#xff0c;强连通图研究图的局部-子图无向图有向图 连通分量强连通分量生成树生成森林边的权&#xff0c;带权图/网几种特殊形…

虚幻学习笔记15—C++和UI(一)

一、前言 在C可以直接创建按钮、滚轮等UI&#xff0c;并且可以直接绑定并处理响应事件。在创建C代码后还是需要通过蓝图来显示到应用中&#xff0c;总体来说还是不如直接用蓝图来的方便。 本文使用的虚幻引擎为5.2.1。 二、实现 2.1、创建UUserWidgetl类型的C类 声明两个按钮…

uniCloud(一) 新建项目、初始化服务空间、云对象访问测试

一、新建一个带有unicloud 二、创建一个服务空间 1. 右键uniCloud&#xff0c;关联云服务空间 我当前没有服务空间&#xff0c;需要新建一个服务空间&#xff0c;之后将其关联。初始化服务空间需要的时间有点长 服务空间初始化成功后&#xff0c;刷新HBuilder&#xff0c;勾选…

Linux系统下CPU性能问题分析案例

&#xff08;上&#xff09; 本文涉及案例来自于学习极客时间专栏《Linux性能优化实战》精心整理而来&#xff0c;案例总结不到位的请各位多多指正。 某个应用的CPU使用率居然达到100%&#xff0c;我该怎么办&#xff1f; 分析过程 使用观察系统CPU使用情况&#xff08;并按下…

03. 医院设置_后端

1、Swagger2 测试工具 编写和维护接口文档是每个程序员的职责&#xff0c;根据Swagger2可以快速帮助我们编写最新的API接口文档&#xff0c;再也不用担心开会前仍忙于整理各种资料了&#xff0c;间接提升了团队开发的沟通效率。 swagger通过注解表明该接口会生成文档&#xf…

vuepress-----25、右侧目录

# 25、vuepress 右侧目录 https://github.com/xuek9900/vuepress-plugin-right-anchor vuepress-plugin-right-anchor English &#xff5c;中文 在用 Vuepress 2.x 编写的文档页面右侧添加 锚点导航栏 # 版本 2.x.x -> Vuepress 2.x -> npm next -> master 分支0…

PS扣印章

1 印章区域图片 2 3 吸取印章上的颜色&#xff0c;调节容差&#xff0c;尽量小一点&#xff0c;过大会将背景也进来 4 CtrlJ 把选区复制出来&#xff0c;这个印章图层比较淡&#xff0c;可以通过多复制几个叠加或通过叠加模式来调节。 5 对几个图层选中后CtrlE合并图层 6 选…

IT圈茶余饭后的“鄙视链”

哈哈&#xff0c;IT圈的鄙视链&#xff0c;简直就是一出情感大戏&#xff01;这个圈子里的人们总是忍不住要互相比较&#xff0c;互相鄙视&#xff0c;仿佛这是一场刺激的游戏&#xff0c;每个人都想要站在鄙视链的最顶端&#xff0c;成为那个最牛逼的存在。 首先&#xff0c;…

深度学习第5天:GAN生成对抗网络

☁️主页 Nowl &#x1f525;专栏 《深度学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​​ 文章目录 一、GAN1.基本思想2.用途3.模型架构 二、具体任务与代码1.任务介绍2.导入库函数3.生成器与判别器4.预处理5.模型训练6.图片生成7.不同训练轮次的结果对比 一…

CSS特效030:日蚀动画

CSS常用示例100专栏目录 本专栏记录的是经常使用的CSS示例与技巧&#xff0c;主要包含CSS布局&#xff0c;CSS特效&#xff0c;CSS花边信息三部分内容。其中CSS布局主要是列出一些常用的CSS布局信息点&#xff0c;CSS特效主要是一些动画示例&#xff0c;CSS花边是描述了一些CSS…