一、问题背景 在开发某个公共应用时,笔者发现该公共应用的数据是所有测试环境(假设存在devdev2dev3)通用的。 这就意味着只需部署一个应用,就能满足所有测试环境的需求;也意味着所有测试环境都需要调用该公共应用,而不同测试环境的应用注册在不同的Nacos命名空间。 二、两种解决方案 如果所有测试环境都需要调用该公共应用,有两种可行的方案。第一种,将该公共服务同时注册到不同的测试环境所对应的命名空间中。 第二种,将公共应用注册到单独的命名空间,不同的测试环境能够跨命名空间访问该应用。 三、详细的问题解决过程 先行交代笔者的版本号配置。Nacos客户端版本号为NACOS1。4。1;Java项目的Nacos版本号如下。 最初想法是将该公共应用同时注册到多个命名空间下。在查找资料的过程中,团队成员在GitHub上发现了一篇类似问题的博客分享:RegistrationCenter:Canservicesindifferentnamespacesbecalledfromeachother?1176。 01注册多个命名空间 从该博客中,我们看到其他程序员朋友也遇到了类似的公共服务的需求。在本篇文章中,笔者将进一步分享实现思路以及示例代码。 说明:以下代码内容来自用户chuntaojun的分享。shareNamespace{namespaceId〔:group〕},{namespaceId〔:group〕}RunWith(SpringRunner。class)SpringBootTest(classesNamingApp。class,properties{server。servlet。contextpathnacos},webEnvironmentSpringBootTest。WebEnvironment。RANDOMPORT)publicclassSelectServiceInShareNamespaceITCase{privateNamingServicenaming1;privateNamingServicenaming2;LocalServerPortprivateintport;Beforepublicvoidinit()throwsException{NamingBase。prepareServer(port);if(naming1null){PropertiespropertiesnewProperties();properties。setProperty(PropertyKeyConst。SERVERADDR,127。0。0。1:port);properties。setProperty(PropertyKeyConst。SHARENAMESPACE,57425802305845079a733229b9f00a36);naming1NamingFactory。createNamingService(properties);Propertiesproperties2newProperties();properties2。setProperty(PropertyKeyConst。SERVERADDR,127。0。0。1:port);properties2。setProperty(PropertyKeyConst。NAMESPACE,57425802305845079a733229b9f00a36);naming2NamingFactory。createNamingService(properties2);}while(true){if(!UP。equals(naming1。getServerStatus())){Thread。sleep(1000L);continue;}break;}}TestpublicvoidtestSelectInstanceInShareNamespaceNoGroup()throwsNacosException,InterruptedException{Stringservice1randomDomainName();Stringservice2randomDomainName();naming1。registerInstance(service1,127。0。0。1,90);naming2。registerInstance(service2,127。0。0。2,90);Thread。sleep(1000);ListInstanceinstancesnaming1。getAllInstances(service2);Assert。assertEquals(1,instances。size());Assert。assertEquals(service2,NamingUtils。getServiceName(instances。get(0)。getServiceName()));}TestpublicvoidtestSelectInstanceInShareNamespaceWithGroup()throwsNacosException,InterruptedException{Stringservice1randomDomainName();Stringservice2randomDomainName();naming2。registerInstance(service1,groupName,127。0。0。1,90);naming3。registerInstance(service2,127。0。0。2,90);Thread。sleep(1000);ListInstanceinstancesnaming3。getAllInstances(service1);Assert。assertEquals(1,instances。size());Assert。assertEquals(service1,NamingUtils。getServiceName(instances。get(0)。getServiceName()));Assert。assertEquals(groupName,NamingUtils。getServiceName(NamingUtils。getGroupName(instances。get(0)。getServiceName())));}} 进一步考虑后发现该解决方案可能不太契合当前遇到的问题。公司目前的开发测试环境有很多个,并且不确定以后会不会继续增加。 如果每增加一个环境,都需要修改一次公共服务的配置,并且重启一次公共服务,着实太麻烦了。倒不如反其道而行,让其他的服务器实现跨命名空间访问公共服务。 02跨命名空间访问 针对实际问题查找资料时,我们找到了类似的参考分享《重写Nacos服务发现逻辑动态修改远程服务IP地址》。 跟着博客思路看代码,笔者了解到服务发现的主要相关类是NacosNamingService,NacosDiscoveryProperties,NacosDiscoveryAutoConfiguration。 然后,笔者将博客的示例代码复制过来,试着进行如下调试:Slf4jConfigurationConditionalOnNacosDiscoveryEnabledConditionalOnProperty(name{spring。profiles。active},havingValuedev)AutoConfigureBefore({NacosDiscoveryClientAutoConfiguration。class})publicclassDevEnvironmentNacosDiscoveryClient{BeanConditionalOnMissingBeanpublicNacosDiscoveryPropertiesnacosProperties(){returnnewDevEnvironmentNacosDiscoveryProperties();}staticclassDevEnvironmentNacosDiscoveryPropertiesextendsNacosDiscoveryProperties{privateNamingServicenamingService;OverridepublicNamingServicenamingServiceInstance(){if(null!this。namingService){returnthis。namingService;}else{PropertiespropertiesnewProperties();properties。put(serverAddr,super。getServerAddr());properties。put(namespace,super。getNamespace());properties。put(com。alibaba。nacos。naming。log。filename,super。getLogName());if(super。getEndpoint()。contains(:)){intindexsuper。getEndpoint()。indexOf(:);properties。put(endpoint,super。getEndpoint()。substring(0,index));properties。put(endpointPort,super。getEndpoint()。substring(index1));}else{properties。put(endpoint,super。getEndpoint());}properties。put(accessKey,super。getAccessKey());properties。put(secretKey,super。getSecretKey());properties。put(clusterName,super。getClusterName());properties。put(namingLoadCacheAtStart,super。getNamingLoadCacheAtStart());try{this。namingServicenewDevEnvironmentNacosNamingService(properties);}catch(Exceptionvar3){log。error(createnamingserviceerror!properties{},e,,this,var3);returnnull;}returnthis。namingService;}}}staticclassDevEnvironmentNacosNamingServiceextendsNacosNamingService{publicDevEnvironmentNacosNamingService(Propertiesproperties){super(properties);}OverridepublicListInstanceselectInstances(StringserviceName,ListStringclusters,booleanhealthy)throwsNacosException{ListInstanceinstancessuper。selectInstances(serviceName,clusters,healthy);instances。stream()。forEach(instanceinstance。setIp(10。101。232。24));returninstances;}}} 调试后发现博客提供的代码并不能满足笔者的需求,还得进一步深入探索。 但幸运的是,调试过程发现Nacos服务发现的关键类是com。alibaba。cloud。nacos。discovery。NacosServiceDiscovery,其中的关键方法是getInstances()和getServices(),即返回指定服务ID的所有服务实例和获取所有服务的名称。 也就是说,对getInstances()方法进行重写肯定能实现本次目标跨命名空间访问公共服务。Returnallinstancesforthegivenservice。paramserviceIdidofservicereturnlistofinstancesthrowsNacosExceptionnacosExceptionpublicListServiceInstancegetInstances(StringserviceId)throwsNacosException{StringgroupdiscoveryProperties。getGroup();ListInstanceinstancesdiscoveryProperties。namingServiceInstance()。selectInstances(serviceId,group,true);returnhostToServiceInstanceList(instances,serviceId);}Returnthenamesofallservices。returnlistofservicenamesthrowsNacosExceptionnacosExceptionpublicListStringgetServices()throwsNacosException{StringgroupdiscoveryProperties。getGroup();ListViewStringservicesdiscoveryProperties。namingServiceInstance()。getServicesOfServer(1,Integer。MAXVALUE,group);returnservices。getData();} 03最终解决思路及代码示例 具体的解决方案思路大致如下:生成一个共享配置类NacosShareProperties,用来配置共享公共服务的namespace和group;重写配置类NacosDiscoveryProperties(新:NacosDiscoveryPropertiesV2),将新增的共享配置类作为属性放进该配置类,后续会用到;重写服务发现类NacosServiceDiscovery(新:NacosServiceDiscoveryV2),这是最关键的逻辑;重写自动配置类NacosDiscoveryAutoConfiguration,将自定义相关类比Nacos原生类更早的注入容器。 最终代码中用到了一些工具类,可以自行补充完整。predescription:共享nacos属性author:rookie0pengdate:202282915:22preConfigurationConfigurationProperties(prefixnacos。share)publicclassNacosShareProperties{privatefinalMapString,SetStringNAMESPACETOGROUPNAMEMAPnewConcurrentHashMap();共享nacos实体列表privateListNacosShareEntityentities;publicListNacosShareEntitygetEntities(){returnentities;}publicvoidsetEntities(ListNacosShareEntityentities){this。entitiesentities;}publicMapString,SetStringgetNamespaceGroupMap(){safeStream(entities)。filter(entitynonNull(entity)nonNull(entity。getNamespace()))。forEach(entity{SetStringgroupNamesNAMESPACETOGROUPNAMEMAP。computeIfAbsent(entity。getNamespace(),knewHashSet());if(nonNull(entity。getGroupNames()))groupNames。addAll(entity。getGroupNames());});returnnewHashMap(NAMESPACETOGROUPNAMEMAP);}OverridepublicStringtoString(){returnNacosShareProperties{entitiesentities};}共享nacos实体publicstaticfinalclassNacosShareEntity{命名空间privateStringnamespace;分组privateListStringgroupNames;publicStringgetNamespace(){returnnamespace;}publicvoidsetNamespace(Stringnamespace){this。namespacenamespace;}publicListStringgetGroupNames(){returngroupNames;}publicvoidsetGroupNames(ListStringgroupNames){this。groupNamesgroupNames;}OverridepublicStringtoString(){returnNacosShareEntity{namespacenamespace,groupNamesgroupNames};}}}description:naocs服务发现属性重写author:rookie0pengdate:20228301:19publicclassNacosDiscoveryPropertiesV2extendsNacosDiscoveryProperties{privatestaticfinalLoggerlogLoggerFactory。getLogger(NacosDiscoveryPropertiesV2。class);privatefinalNacosSharePropertiesnacosShareProperties;privatestaticfinalMapString,NamingServiceNAMESPACETONAMINGSERVICEMAPnewConcurrentHashMap();publicNacosDiscoveryPropertiesV2(NacosSharePropertiesnacosShareProperties){super();this。nacosSharePropertiesnacosShareProperties;}publicMapString,NamingServiceshareNamingServiceInstances(){if(!NAMESPACETONAMINGSERVICEMAP。isEmpty()){returnnewHashMap(NAMESPACETONAMINGSERVICEMAP);}ListNacosShareProperties。NacosShareEntityentitiesOptional。ofNullable(nacosShareProperties)。map(NacosShareProperties::getEntities)。orElse(Collections。emptyList());entities。stream()。filter(entitynonNull(entity)nonNull(entity。getNamespace()))。filter(PredicateUtil。distinctByKey(NacosShareProperties。NacosShareEntity::getNamespace))。forEach(entity{try{NamingServicenamingServiceNacosFactory。createNamingService(getNacosProperties(entity。getNamespace()));if(namingService!null){NAMESPACETONAMINGSERVICEMAP。put(entity。getNamespace(),namingService);}}catch(Exceptione){log。error(createnamingserviceerror!properties{},e,this,e);}});returnnewHashMap(NAMESPACETONAMINGSERVICEMAP);}privatePropertiesgetNacosProperties(Stringnamespace){PropertiespropertiesnewProperties();properties。put(SERVERADDR,getServerAddr());properties。put(USERNAME,Objects。toString(getUsername(),));properties。put(PASSWORD,Objects。toString(getPassword(),));properties。put(NAMESPACE,namespace);properties。put(UtilAndComs。NACOSNAMINGLOGNAME,getLogName());StringendpointgetEndpoint();if(endpoint。contains(:)){intindexendpoint。indexOf(:);properties。put(ENDPOINT,endpoint。substring(0,index));properties。put(ENDPOINTPORT,endpoint。substring(index1));}else{properties。put(ENDPOINT,endpoint);}properties。put(ACCESSKEY,getAccessKey());properties。put(SECRETKEY,getSecretKey());properties。put(CLUSTERNAME,getClusterName());properties。put(NAMINGLOADCACHEATSTART,getNamingLoadCacheAtStart());enrichNacosDiscoveryProperties(properties);returnproperties;}}description:naocs服务发现重写author:rookie0pengdate:20228301:10publicclassNacosServiceDiscoveryV2extendsNacosServiceDiscovery{privatefinalNacosDiscoveryPropertiesV2discoveryProperties;privatefinalNacosSharePropertiesnacosShareProperties;privatefinalNacosServiceManagernacosServiceManager;publicNacosServiceDiscoveryV2(NacosDiscoveryPropertiesV2discoveryProperties,NacosSharePropertiesnacosShareProperties,NacosServiceManagernacosServiceManager){super(discoveryProperties,nacosServiceManager);this。discoveryPropertiesdiscoveryProperties;this。nacosSharePropertiesnacosShareProperties;this。nacosServiceManagernacosServiceManager;}Returnallinstancesforthegivenservice。paramserviceIdidofservicereturnlistofinstancesthrowsNacosExceptionnacosExceptionpublicListServiceInstancegetInstances(StringserviceId)throwsNacosException{StringgroupdiscoveryProperties。getGroup();ListInstanceinstancesdiscoveryProperties。namingServiceInstance()。selectInstances(serviceId,group,true);if(isEmpty(instances)){MapString,SetStringnamespaceGroupMapnacosShareProperties。getNamespaceGroupMap();MapString,NamingServicenamespace2NamingServiceMapdiscoveryProperties。shareNamingServiceInstances();for(Map。EntryString,NamingServiceentry:namespace2NamingServiceMap。entrySet()){Stringnamespace;NamingServicenamingService;if(isNull(namespaceentry。getKey())isNull(namingServiceentry。getValue()))continue;SetStringgroupNamesnamespaceGroupMap。get(namespace);ListInstanceshareInstances;if(isEmpty(groupNames)){shareInstancesnamingService。selectInstances(serviceId,group,true);if(nonEmpty(shareInstances))break;}else{shareInstancesnewArrayList();for(StringgroupName:groupNames){ListInstancesubShareInstancesnamingService。selectInstances(serviceId,groupName,true);if(nonEmpty(subShareInstances)){shareInstances。addAll(subShareInstances);}}}if(nonEmpty(shareInstances)){instancesshareInstances;break;}}}returnhostToServiceInstanceList(instances,serviceId);}Returnthenamesofallservices。returnlistofservicenamesthrowsNacosExceptionnacosExceptionpublicListStringgetServices()throwsNacosException{StringgroupdiscoveryProperties。getGroup();ListViewStringservicesdiscoveryProperties。namingServiceInstance()。getServicesOfServer(1,Integer。MAXVALUE,group);returnservices。getData();}publicstaticListServiceInstancehostToServiceInstanceList(ListInstanceinstances,StringserviceId){ListServiceInstanceresultnewArrayList(instances。size());for(Instanceinstance:instances){ServiceInstanceserviceInstancehostToServiceInstance(instance,serviceId);if(serviceInstance!null){result。add(serviceInstance);}}returnresult;}publicstaticServiceInstancehostToServiceInstance(Instanceinstance,StringserviceId){if(instancenull!instance。isEnabled()!instance。isHealthy()){returnnull;}NacosServiceInstancenacosServiceInstancenewNacosServiceInstance();nacosServiceInstance。setHost(instance。getIp());nacosServiceInstance。setPort(instance。getPort());nacosServiceInstance。setServiceId(serviceId);MapString,StringmetadatanewHashMap();metadata。put(nacos。instanceId,instance。getInstanceId());metadata。put(nacos。weight,instance。getWeight());metadata。put(nacos。healthy,instance。isHealthy());metadata。put(nacos。cluster,instance。getClusterName());metadata。putAll(instance。getMetadata());nacosServiceInstance。setMetadata(metadata);if(metadata。containsKey(secure)){booleansecureBoolean。parseBoolean(metadata。get(secure));nacosServiceInstance。setSecure(secure);}returnnacosServiceInstance;}privateNamingServicenamingService(){returnnacosServiceManager。getNamingService(discoveryProperties。getNacosProperties());}}description:重写nacos服务发现的自动配置author:rookie0pengdate:20228301:08Configuration(proxyBeanMethodsfalse)ConditionalOnDiscoveryEnabledConditionalOnNacosDiscoveryEnabledAutoConfigureBefore({NacosDiscoveryAutoConfiguration。class})publicclassNacosDiscoveryAutoConfigurationV2{BeanConditionalOnMissingBeanpublicNacosDiscoveryPropertiesV2nacosProperties(NacosSharePropertiesnacosShareProperties){returnnewNacosDiscoveryPropertiesV2(nacosShareProperties);}BeanConditionalOnMissingBeanpublicNacosServiceDiscoverynacosServiceDiscovery(NacosDiscoveryPropertiesV2discoveryPropertiesV2,NacosSharePropertiesnacosShareProperties,NacosServiceManagernacosServiceManager){returnnewNacosServiceDiscoveryV2(discoveryPropertiesV2,nacosShareProperties,nacosServiceManager);}} 本以为问题到这就结束了,但最后自测时发现程序根本不走Nacos的服务发现逻辑,而是执行Ribbon的负载均衡逻辑com。netflix。loadbalancer。AbstractLoadBalancerRule。 不过实现类是com。alibaba。cloud。nacos。ribbon。NacosRule,继续基于NacosRule重写负载均衡。description:共享nacos命名空间规则author:rookie0pengdate:20228312:04publicclassShareNacosNamespaceRuleextendsAbstractLoadBalancerRule{privatestaticfinalLoggerLOGGERLoggerFactory。getLogger(ShareNacosNamespaceRule。class);AutowiredprivateNacosDiscoveryPropertiesV2nacosDiscoveryPropertiesV2;AutowiredprivateNacosSharePropertiesnacosShareProperties;重写choose方法paramkeyreturnSneakyThrowsOverridepublicServerchoose(Objectkey){try{StringclusterNamethis。nacosDiscoveryPropertiesV2。getClusterName();DynamicServerListLoadBalancerloadBalancer(DynamicServerListLoadBalancer)getLoadBalancer();StringnameloadBalancer。getName();NamingServicenamingServicenacosDiscoveryPropertiesV2。namingServiceInstance();ListInstanceinstancesnamingService。selectInstances(name,true);if(CollectionUtils。isEmpty(instances)){LOGGER。warn(noinstanceinservice{},thentogetshareservicesinstance,name);ListInstanceshareNamingServicethis。getShareNamingService(name);if(nonEmpty(shareNamingService))instancesshareNamingService;elsereturnnull;}ListInstanceinstancesToChooseinstances;if(org。apache。commons。lang3。StringUtils。isNotBlank(clusterName)){ListInstancesameClusterInstancesinstances。stream()。filter(instanceObjects。equals(clusterName,instance。getClusterName()))。collect(Collectors。toList());if(!CollectionUtils。isEmpty(sameClusterInstances)){instancesToChoosesameClusterInstances;}else{LOGGER。warn(Acrossclustercalloccurs,name{},clusterName{},instance{},name,clusterName,instances);}}InstanceinstanceExtendBalancer。getHostByRandomWeight2(instancesToChoose);returnnewNacosServer(instance);}catch(Exceptione){LOGGER。warn(NacosRuleerror,e);returnnull;}}OverridepublicvoidinitWithNiwsConfig(IClientConfigiClientConfig){}privateListInstancegetShareNamingService(StringserviceId)throwsNacosException{ListInstanceinstancesCollections。emptyList();MapString,SetStringnamespaceGroupMapnacosShareProperties。getNamespaceGroupMap();MapString,NamingServicenamespace2NamingServiceMapnacosDiscoveryPropertiesV2。shareNamingServiceInstances();for(Map。EntryString,NamingServiceentry:namespace2NamingServiceMap。entrySet()){Stringnamespace;NamingServicenamingService;if(isNull(namespaceentry。getKey())isNull(namingServiceentry。getValue()))continue;SetStringgroupNamesnamespaceGroupMap。get(namespace);ListInstanceshareInstances;if(isEmpty(groupNames)){shareInstancesnamingService。selectInstances(serviceId,true);if(nonEmpty(shareInstances))break;}else{shareInstancesnewArrayList();for(StringgroupName:groupNames){ListInstancesubShareInstancesnamingService。selectInstances(serviceId,groupName,true);if(nonEmpty(subShareInstances)){shareInstances。addAll(subShareInstances);}}}if(nonEmpty(shareInstances)){instancesshareInstances;break;}}returninstances;}} 至此问题得以解决。 在Nacos上配置好共享namespace和group后,就能够进行跨命名空间访问了。nacos共享命名空间配置示例nacos。share。entities〔0〕。namespacee6ed20173ed64d9b824adb626424fc7bnacos。share。entities〔0〕。groupNames〔0〕DEFAULTGROUP指定服务使用共享的负载均衡规则,serviceid是注册到nacos上的服务id,ShareNacosNamespaceRule需要写全限定名serviceid。ribbon。NFLoadBalancerRuleClassName。。。ShareNacosNamespaceRule 注意:如果Java项目的nacosdiscovery版本用的是2021。1,则不需要重写Ribbon的负载均衡类,因为该版本的Nacos不依赖Ribbon。 2。2。1。RELEASE版本的nacosdiscovery依赖Ribbon。 2021。1版本的nacosdiscovery不依赖Ribbon。 四、总结 为了达到共享命名空间的预期,构思、查找资料、实现逻辑、调试,前后一共花费4天时间。成就感满满的同时,笔者也发现该功能仍存在共享服务缓存等可优化空间,留待后续实现。