Nacos整合之服务注册编码实践

540 阅读10分钟

本章节的讲解代码下载地址如下:

链接: https://pan.baidu.com/s/1FLwq5Tes8acVNVRBC_Ut1Q 
提取码: swmc

本章节正式进入编码环节,使用 Spring Cloud Alibaba 套件整合 Nacos 组件。本章节会编写一个服务实例并将其注册至 Nacos 服务中心,重要知识点为代码整合步骤、Nacos 服务中心相关的配置项和服务的自动注册过程讲解。

编写服务代码

《实战基础 (二):Spring Boot 开发介绍及 Spring Cloud Alibaba 模板项目构建》中已经把 Spring Cloud Alibaba 模板项目写好了,这里可以直接拿过来,以此为基础进行功能改造。因为是编写与 Nacos 相关的代码,这里就把模板项目 spring-cloud-alibaba-demo 的名称改为 spring-cloud-alibaba-nacos-demo,root 节点的 pom.xml 文件内容也修改掉,代码如下:

<artifactId>spring-cloud-alibaba-nacos-demo</artifactId>
<版本>0.0.1-快照</版本>
<name>spring-cloud-alibaba-nacos-demo</name>
<包装>pom</包装>
<description>Spring Cloud Alibaba Nacos Demo</description>


然后新建一个 Module,命名为 nacos-provider-demo,Java 代码的包名为 ltd.newbee.cloud。在该 Module 的 pom.xml 配置文件中增加 parent 标签,与上层 Maven 建立好关系。之后,在这个子模块的 pom.xml 文件中加入 Nacos 的依赖项 spring-cloud-starter-alibaba-nacos-discovery。最终,子节点 nacos-provider-demo 的 pom.xml 如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <模型版本>4.0.0</模型版本>
    <groupId>ltd.newbee.cloud</groupId>
    <artifactId>nacos-provider-demo</artifactId>
    <版本>0.0.1-快照</版本>
    <name>nacos-provider-demo</name>
    <description>Spring Cloud Alibaba Provider Demo</description>

    <父母>
        <groupId>ltd.newbee.cloud</groupId>
        <artifactId>spring-cloud-alibaba-nacos-demo</artifactId>
        <版本>0.0.1-快照</版本>
    </父母>

    <属性>
        <java.version>1.8</java.version>
    </属性>

    <依赖关系>
        <依赖关系>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </依赖>

        <依赖关系>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <范围>测试</范围>
        </依赖>

        <依赖关系>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </依赖>
                
    </依赖项>
</项目>


接着在 nacos-provider-demo 中进行简单的功能编码,首先把该 Spring Boot 项目的端口号设置为 8091。之后创建 ltd.newbee.cloud.api 包,在 api 包中新建 NewBeeCloudGoodsAPI 类,代码如下:

ltd.newbee.cloud.api;

导入 org.springframework.beans.factory.annotation.Value;
导入 org.springframework.web.bind.annotation.GetMapping;
导入 org.springframework.web.bind.annotation.RestController;

@RestController
公共类 NewBeeCloudGoodsAPI {

    @Value("${server.port}")
    私有字符串应用程序服务器端口;

    @GetMapping("/goodsServiceTest")
    公共字符串商品服务测试(){
        
        返回“这是来自端口的商品服务:”+ applicationServerPort;
    }
}


启动类命名为 ProviderApplication,代码如下:

package ltd.newbee.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }

}


基础编码完成,此时 nacos-provider-demo 的目录结构如下图所示:

配置文件中添加 Nacos 配置参数

完成基础的服务编码后,接下来就要把这个服务注册到 Nacos 中去。过程也非常简单,只需要在 application.properties 文件中添加几个 Nacos 的配置项即可。

添加了 Nacos 配置项之后的 application.properties 配置文件如下:

# 项目启动端口
server.port=8091
# 应用名称
spring.application.name=newbee-cloud-goods-service
# 注册中心Nacos的访问地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
# 登录名(默认username,可自行修改)
spring.cloud.nacos.username=nacos
# 密码(默认password,可自行修改)
spring.cloud.nacos.password=nacos


这样,启动项目时,服务就能够自动注册到 Nacos 服务中心了。

当然,Spring Cloud 中与 Nacos 服务发现功能相关的配置项不止这三个。笔者查了一下 Spring Cloud Alibaba 2021.0.1.0 版的源码,与之相关的配置项共有 31 个,在 spring-cloud-starter-alibaba-nacos-discovery-2021.0.1.0.jar 中的 spring-configuration-metadata.json 文件中可以查看,如下图所示。

都是以 “spring.cloud.nacos.discovery.” 开头的配置项,这里节选了部分常用的配置项,整理如下:

配置项key默认值说明
服务端地址spring.cloud.nacos.discovery.server-addr
服务名spring.cloud.nacos.discovery.service${spring.application.name}注册到 Nacos 上的名称,默认值为应用名称,一般不用配置
权重spring.cloud.nacos.discovery.weight1取值范围 1 到 100,数值越大,权重越大
网卡名spring.cloud.nacos.discovery.network-interface当 IP 未配置时,注册的 IP 为此网卡所对应的 IP 地址,如果此项也未配置,则默认取第一块网卡的地址
注册的 IP 地址spring.cloud.nacos.discovery.ip优先级最高
注册的端口spring.cloud.nacos.discovery.port-1默认情况下不用配置,会自动探测
是否为临时服务spring.cloud.nacos.discovery.ephemeraltrue默认为 true,即临时服务。如果值为 false,则表示为永久服务,这种服务在注册时不会向 Nacos Server 发送心跳信息
心跳的时间间隔spring.cloud.nacos.discovery.heart-beat-interval5000时间单位是 ms,默认为 5 秒,可自行修改
心跳的超时时间spring.cloud.nacos.discovery.heart-beat-timeout15000时间单位是 ms,默认为 15 秒,可自行修改
命名空间spring.cloud.nacos.discovery.namespace常用场景之一是不同环境的注册的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
AccessKeyspring.cloud.nacos.discovery.access-key
SecretKeyspring.cloud.nacos.discovery.secret-key
Metadataspring.cloud.nacos.discovery.metadata使用 Map 格式配置
日志文件名spring.cloud.nacos.discovery.log-name
接入点spring.cloud.nacos.discovery.endpoint地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址
是否启用 Nacosspring.cloud.nacos.discovery.register-enabledtrue默认启动,设置为 false 时会关闭自动向 Nacos 注册的功能

除此之外,Nacos 作为注册中心时还有很多配置项,这个在后面的章节中会继续介绍。

接下来,需要启动 Nacos Server,然后验证本次的服务注册功能。

功能验证

Nacos Server 启动成功后,就可以接着启动 nacos-provider-demo 项目了。如果一切正常,启动成功可以在控制台看到如下的日志输出:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-04-22 22:37:09.772  INFO 6332 --- [           main] ltd.newbee.cloud.ProviderApplication     : Starting ProviderApplication using Java 1.8.0_291
2022-04-22 22:37:09.774  INFO 6332 --- [           main] ltd.newbee.cloud.ProviderApplication     : No active profile set, falling back to default profiles: default
2022-04-22 22:37:10.277  INFO 6332 --- [           main] o.s.cloud.context.scope.GenericScope     : BeanFactory id=3501c8aa-6b71-3be5-a30a-94e7ad619e89
2022-04-22 22:37:10.602  INFO 6332 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8091 (http)
2022-04-22 22:37:10.612  INFO 6332 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-04-22 22:37:10.612  INFO 6332 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.56]
2022-04-22 22:37:10.691  INFO 6332 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-04-22 22:37:10.691  INFO 6332 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 875 ms
2022-04-22 22:37:11.444  INFO 6332 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8091 (http) with context path ''
2022-04-22 22:37:11.457  INFO 6332 --- [           main] c.a.c.n.registry.NacosServiceRegistry    : nacos registry, DEFAULT_GROUP newbee-cloud-goods-service 192.168.1.105:8091 register finished
2022-04-22 22:37:11.566  INFO 6332 --- [           main] ltd.newbee.cloud.ProviderApplication     : Started ProviderApplication in 2.356 seconds (JVM running for 2.713)


如果未能成功启动,开发者就需要查看控制台中的日志是否报错,并及时确认问题和修复。

进入 Nacos 控制台,点击 “服务管理” 中的服务列表,可以看到列表中已经存在一条 newbee-cloud-goods-service 的服务信息,证明注册成功。

newbee-cloud-goods-service 服务的详情页面如下图所示。

Spring Cloud Alibaba 官方给出的验证方法是直接访问 Nacos Server 的 openAPI。比如,当前的服务名称是 newbee-cloud-goods-service,可以直接访问下方的这个链接来查看这个服务的信息:

[与 Nacos 控制台页面中的内容相比,使用这种方式获得的信息更详细一些,比如心跳的时间配置也显示了。不管哪种方式,目的就是确认这个服务注册是否成功。](link.juejin.cn/?target=htt… "http://localhost:8848/nacos/v1/ns/catalog/instances?service>http://localhost:8848/nacos/v1/ns/catalog/instances?serviceName=newbee-cloud-goods-service&clusterName=DEFAULT&pageSize=10&pageNo=1&namespaceId=

如果注册成功,可以获取到如下结果:

``` xxx is not found!; ```

如果服务未注册成功,则会获得如下响应信息:

xxx is not found!;


到这里,服务注册的配置和验证就完成了。

但是,应该有不少读者会发出疑问:就这么简单?就这?就这?

有些接触过微服务项目开发的读者也会问:这个作者是不是漏了什么步骤?@EnableDiscoveryClient 注解哪儿去了?没有 @EnableDiscoveryClient 也能让服务注册成功吗?

Nacos 服务注册源码解析

为了解答上述的几个问题,就要结合源码和 Spring Boot 框架的自动装配(Auto Configuration)机制来讲解了。

首先,在 nacos-provider-demo 项目的启动日志中,有这么一行日志:

2022-04-22 22:37:11.457  INFO 6332 --- [           main] c.a.c.n.registry.NacosServiceRegistry    : nacos registry, DEFAULT_GROUP newbee-cloud-goods-service 192.168.1.105:8091 register finished


这行日志告诉了开发者,服务注册的步骤已经完成了,时间点是 Servlet 容器启动之后。除此之外,就没有其它信息了,开发者如果刚开始接触,肯定会有一点懵。服务是什么时候注册的?服务又是怎么注册的?服务注册时做了什么?

好的,顺着这条日志来找找上面这三个问题的答案吧。这行日志是在 NacosServiceRegistry 类中打印出来的,全局搜索 “NacosServiceRegistry”,最终看到这个类的全路径为 com.alibaba.cloud.nacos.registry.NacosServiceRegistry。很明显,也在 spring-cloud-starter-alibaba-nacos-discovery-2021.0.1.0.jar 中,如下图所示。

接下来,在 com.alibaba.cloud.nacos.registry.NacosServiceRegistry 类的第 75 行(也就是打印日志的这一行)打一个断点,然后以 debug 模式启动项目。之后,启动的步骤就停在了这里,如下图所示。

找到了本次自动装配的主角:NacosServiceRegistryAutoConfiguration 类。这是 Nacos 服务注册的自动装配类,源码如下(已省略部分代码)。

package com.alibaba.cloud.nacos.registry;


@Configuration(proxyBeanMethods = false) 

@EnableConfigurationProperties 

@ConditionalOnNacosDiscoveryEnabled 

@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
		matchIfMissing = true)

@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class,
		AutoServiceRegistrationAutoConfiguration.class,
		NacosDiscoveryAutoConfiguration.class })
public class NacosServiceRegistryAutoConfiguration {

	@Bean 
	@ConditionalOnBean(AutoServiceRegistrationProperties.class) 
	public NacosRegistration nacosRegistration(
			ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
			NacosDiscoveryProperties nacosDiscoveryProperties,
			ApplicationContext context) {
		return new NacosRegistration(registrationCustomizers.getIfAvailable(),
				nacosDiscoveryProperties, context);
	}

	@Bean  
	@ConditionalOnBean(AutoServiceRegistrationProperties.class) 
	public NacosAutoServiceRegistration nacosAutoServiceRegistration(
			NacosServiceRegistry registry,
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			NacosRegistration registration) {
		return new NacosAutoServiceRegistration(registry,
				autoServiceRegistrationProperties, registration);
	}

}


NacosServiceRegistryAutoConfiguration 类的注解释义如下所示。

@Configuration(proxyBeanMethods = false):指定该类为配置类。

@ConditionalOnNacosDiscoveryEnabled:点击进入该注解的源码可知,会判断当前绑定属性中 spring.cloud.nacos.discovery.enabled 的值,值为 true 时则生效,默认为 true。

@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true):判断当前绑定属性中 spring.cloud.service-registry.auto-registration.enabled 的值,值为 true 时则生效,默认为 true。

由源码可知,NacosServiceRegistryAutoConfiguration 自动配置类的生效条件是 spring.cloud.service-registry.auto-registration.enabled=true 并且 spring.cloud.nacos.discovery.enabled=true。而这两个配置项的默认值都是 true,即使不做任何配置,NacosServiceRegistryAutoConfiguration 自动配置类的生效条件也是成立的。除非开发者在 application.properties 配置文件中把这两个配置项设置为 false,不然,一定会触发自动装配和自动注册服务的。

Spring Boot 项目在启动过程中,完成了自动装配的工作。NacosServiceRegistryAutoConfiguration 自动配置完成后,最终调用到了 NacosServiceRegistry 的 register() 方法完成了向 Nacos 注册服务的过程。这也就解释了,为什么没有在启动类上添加 @EnableDiscoveryClient 注解也能完成服务注册的步骤,因为在新版本中已经默认了会自动注册服务。当然,在使用 Spring Cloud Alibaba 套件之前的版本时,还是需要在启动类上添加 @EnableDiscoveryClient 注解开启对应的功能。在新版本中,可以添加 @EnableDiscoveryClient 注解,也可以不添加,并不会报错。

以下是 Spring Cloud Alibaba 官方文档中的解释,读者可以结合上面的源码解析一起理解:

Spring Cloud Nacos Discovery 遵循了 spring cloud common 标准,实现了 AutoServiceRegistration、ServiceRegistry、Registration 这三个接口。

在 Spring Cloud 应用的启动阶段,监听了 WebServerInitializedEvent 事件,当 Web 容器初始化完成后,即收到 WebServerInitializedEvent 事件后,会触发注册的动作,调用 ServiceRegistry 的 register() 方法,将服务注册到 Nacos Server。

com.alibaba.cloud.nacos.registry.NacosServiceRegistry 是 ServiceRegistry 接口的具体实现类,所以实际调用的是 NacosServiceRegistry 类中的 register() 方法。继续跟入源码,会发现该方法最终调用的是 com.alibaba.nacos.client.naming.NacosNamingService 类中的 registerInstance() 方法,源码及注释如下:

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    
    serverProxy.registerService(groupedServiceName, groupName, instance);
}


继续跟入源码,首先是注册服务的方法 registerService(),该方法位于 com.alibaba.nacos.client.naming.net.NamingProxy 类中,源码及注释如下:

    
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                instance);
        
        /
        final Map<String, String> params = new HashMap<String, String>(16);
        
        
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, serviceName);
        params.put(CommonParams.GROUP_NAME, groupName);
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
        
        
        
        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
    }


然后是发送心跳信息的方法 addBeatInfo(),该方法位于 com.alibaba.nacos.client.naming.beat.BeatReactor 类中,源码如下:

    
    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }


线程类 BeatTask 为 com.alibaba.nacos.client.naming.beat.BeatReactor 的内部类,源码及注释如下:

    class BeatTask implements Runnable {
        
        BeatInfo beatInfo;
        
        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }
        
        @Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
                
                JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
                long interval = result.get("clientBeatInterval").asLong();
                boolean lightBeatEnabled = false;
                if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
                    lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
                }
                BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                if (interval > 0) {
                    nextTime = interval;
                }
                int code = NamingResponseCode.OK;
                if (result.has(CommonParams.CODE)) {
                    code = result.get(CommonParams.CODE).asInt();
                }
                
                if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                    Instance instance = new Instance();
                    instance.setPort(beatInfo.getPort());
                    instance.setIp(beatInfo.getIp());
                    instance.setWeight(beatInfo.getWeight());
                    instance.setMetadata(beatInfo.getMetadata());
                    instance.setClusterName(beatInfo.getCluster());
                    instance.setServiceName(beatInfo.getServiceName());
                    instance.setInstanceId(instance.getInstanceId());
                    instance.setEphemeral(true);
                    try {
                        serverProxy.registerService(beatInfo.getServiceName(),
                                NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                    } catch (Exception ignore) {
                    }
                }
            } catch (NacosException ex) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                        JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
    
            } catch (Exception unknownEx) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",
                        JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);
            } finally {
                
                executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
            }
        }
    }


最终,结合源码分析可知,服务实例在启动时会自动注册到 Nacos Server 中,同时开启心跳检测线程,定时向 Nacos Server 同步服务信息。笔者也整理了一张服务注册至 Nacos Server 中的简图方便理解,如下图所示。

总结

到这里,服务注册相关的编码和功能讲解就完成了。最后,笔者也通过项目启动时的一行日志,结合源码分析了服务注册的完整流程。如果觉得查看源码比较吃力,那么只需要知道默认情况下在 Spring Cloud Alibaba 2021.x 版本中服务启动后会自动向 Nacos Server 发起注册流程即可。如果想要了解服务注册背后的原理,建议读者可以根据本章节中整理的源码分析过程和提到的几个具体实现类,自行查看源码并通过 debug 模式来复盘服务的自动注册流程。读者如果有任何问题或者想要和笔者讨论的内容,都可以在评论区留下看法,笔者会根据读者的反馈和问题继续整理和完善本章节内容。

就像与微信与远方的朋友视频通话一样,一方已经发起视频通话了,对面的朋友也应该点击接听了。下一章,会继续讲解服务治理中的服务发现流程,并通过编码讲解引入服务中心后的服务通信过程。

原文地址 juejin.cn