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

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

编写服务代码

前面章节中已经把 Spring Cloud Alibaba 模板项目创建完成,这里可以直接拿过来用,以此为基础进行功能改造。因为是编写与 Nacos 相关的代码,所以这里先把模板项目 spring-cloud-alibaba-demo 的名称改为 spring-cloud-alibaba-nacos-demoroot 节点的 pom.xml 文件内容也修改一下,代码如下:

<artifactId>spring-cloud-alibaba-nacos-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-alibaba-nacos-demo</name>
<packaging>pom</packaging>
<description>Spring Cloud Alibaba Nacos Demo</description>

然后新建一个模块,命名为 nacos-provider-demoJava 代码的包名为 ltd.newbee.cloud

在该模块的 pom.xml 配置文件中增加 parent 标签,与上层 Maven 建立好关系。接着在这个子模块的 pom.xml 文件中加入 Nacos 的依赖项 spring-cloud-starter-alibaba-nacos-discovery。最终子节点 nacos-provider-demopom.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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ltd.newbee.cloud</groupId>
    <artifactId>nacos-provider-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>nacos-provider-demo</name>
    <description>Spring Cloud Alibaba Provider Demo</description>

    <parent>
        <groupId>ltd.newbee.cloud</groupId>
        <artifactId>spring-cloud-alibaba-nacos-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
</project>

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

package ltd.newbee.cloud.api;

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

@RestController
public class NewBeeCloudGoodsAPI {

    @Value("${server.port}")
    private String applicationServerPort; // 读取当前应用的启动端口

    @GetMapping("/goodsServiceTest")
    public String goodsServiceTest() {
        // 返回信息给调用端
        return "this is goodsService from port:" + 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 的目录结构如图 6-8 所示。

image 2025 04 16 11 32 12 341
Figure 1. 图6-8 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 服务发现功能相关的配置项不止这三个。笔者查了一下 SpringCloud Alibaba 2021.0.1.0 版的源码,与之相关的配置项共有 31 个,在 spring-cloud-starter-alibaba-nacos-discovery-2021.0.1.0.jarspring-configuration-metadata.json 文件中可以查看,如图 6-9 所示。

都是以 “spring.cloud.nacos.discovery.” 开头的配置项,这里节选了部分常用的配置项,如表 6-1 所示。

image 2025 04 16 11 34 14 610
Figure 2. 图6-9 与服务发现功能相关的配置项源码截图
配置项 key 默认值 说明

服务端地址

spring.cloud.nacos.discovery.server-addr

服务名

spring.cloud.nacos.discovery.service

${spring.application.name}

注册到 Nacos 上的名称,默认值为应用名,一般不用配置

权重

spring.cloud.nacos.discovery.weight

1

取值范围为 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.ephemeral

true

默认为 true,即临时服务。如果值为 false,则表示永久服务,这种服务在注册时不会向 Nacos Server 发送 “心跳” 信息

“心跳”的时间间隔

spring.cloud.nacos.discovery.heart-beat-interval

5000

时间单位是 ms,默认为 5 秒,可自行修改

“心跳” 的超时时间

spring.cloud.nacos.discovery.heart-beat-timeout

15000

时间单位是 ms,默认为 15 秒,可自行修改

命名空间

spring.cloud.nacos.discovery.namespace

常用场景之一是不同环境的注册中心隔离,如开发测试环境和生产环境的资源(如配置、服务)隔离等

AccessKey

spring.cloud.nacos.discovery.access-key

SecretKey

spring.cloud.nacos.discovery.secret-key

Metadata

spring.cloud.nacos.discovery.metadata

使用 Map 格式配置

日志文件名

spring.cloud.nacos.discovery.log-name

接入点

spring.cloud.nacos.discovery.endpoint

地域的某个服务的入口域名,通过此域名可以动态地获得服务地址

是否启用 Nacos

spring.cloud.nacos.discovery.register-enabled

true

默认启动,设置为 false 时会关闭向 Nacos 注册的功能

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

服务注册功能验证

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

image 2025 04 16 11 45 14 554

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

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

image 2025 04 16 11 45 57 763
Figure 3. 图6-10 Nacos 控制台中的“服务列表”页面

newbee-cloud-goods-service 服务信息的详情页面如图 6-11 所示。

image 2025 04 16 11 46 51 274
Figure 4. 图6-11 newbee-cloud-goods-service 服务信息的详情页面

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

http://localhost:8848/nacos/v1/ns/catalog/instances?serviceName=newbee-cloud-goods-service&clusterName=DEFAULT&pageSize=10&pageNo=1&namespaceId=

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

{"list": [{"instanceId": "192.168.1.105#8091#DEFAULT#DEFAULT_GROUP@@newbee-cloud-goods-service", "ip": "192.168.1.105", "port": 8091, "weight": 1.0, "healthy": true, "enabled": true, "ephemeral": true, "clusterName": "DEFAULT", "serviceName": "DEFAULT_GROUP@@newbee-cloud-goods-service", "metadata": {"preserved.register.source": "SPRING_CLOUD"}, "lastBeat": 1648392838653, "marked": false, "app": "unknown", "instanceHeartBeatInterval": 5000, "instanceHeartBeatTimeout": 15000, "ipDeleteTimeout": 30000}], "count": 1}

Nacos 控制台页面中的内容相比,使用这种方式获得的信息更详细一些,如 “心跳” 的时间配置也显示了。不管使用哪种方式,目的都是确认这个服务注册是否成功。

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

xxx is not found!;

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

Nacos服务注册源码解析

接触过微服务架构项目开发的读者可能会问:作者是不是少了什么步骤?@EnableDiscoveryClient 注解怎么没了?没有 @EnableDiscoveryClient 也能让服务注册成功吗?

接下来笔者结合源码和 Spring Boot 框架的自动装配(Auto Configuration)机制讲解服务发现流程。

nacos-provider-demo 项目的启动日志中,有这么一条日志:

2023-06-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 中,如图 6-12 所示。

image 2025 04 16 11 52 09 295
Figure 5. 图6-12 NacosServiceRegistry 类的源码截图

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

image 2025 04 16 11 52 42 947
Figure 6. 图6-13 在 NacosServiceRegistry 类源码的第75行打上断点

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

package com.alibaba.cloud.nacos.registry;

// 配置类
@Configuration(proxyBeanMethods = false)
// 属性值配置
@EnableConfigurationProperties
// 自动配置生效条件 1
@ConditionalOnNacosDiscoveryEnabled
// 自动配置生效条件 2
@ConditionalOnProperty(value =
        "spring.cloud.service-registry.auto-registration.enabled",
        matchIfMissing = true)
// 自动配置时机在 AutoServiceRegistrationConfiguration、
// AutoServiceRegistrationAutoConfiguration、NacosDiscoveryAutoConfiguration 之后
@AutoConfigureAfter({AutoServiceRegistrationConfiguration.class,
        AutoServiceRegistrationAutoConfiguration.class,
        NacosDiscoveryAutoConfiguration.class})
public class NacosServiceRegistryAutoConfiguration {
    @Bean // 注册 NacosRegistration 到 IoC 容器中
    @ConditionalOnBean(AutoServiceRegistrationProperties.class)
    // 当前 IoC 容器中存在 AutoServiceRegistrationProperties 类型的 Bean 时该 Bean 时注册
    public NacosRegistration nacosRegistration(
            ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
            NacosDiscoveryProperties nacosDiscoveryProperties,
            ApplicationContext context) {
        return new NacosRegistration(registrationCustomizers.getIfAvailable(),
                nacosDiscoveryProperties, context);
    }

    @Bean // 注册 NacosAutoServiceRegistration 到 IoC 容器中
    @ConditionalOnBean(AutoServiceRegistrationProperties.class)
    // 当前 IoC容器中存在 AutoServiceRegistrationProperties 类型的 Bean 时该 Bean 时注册
    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=truespring.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 标准,实现了 AutoServiceRegistrationServiceRegistryRegistration 这三个接口。

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

com.alibaba.cloud.nacos.registry.NacosServiceRegistryServiceRegistry 接口的具体实现类,因此实际调用的是 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);
    //是否为临时服务。默认为临时服务,即默认发送“心跳”信息至 Nacos Server
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        //创建“心跳”线程,向 Nacos Server 发送“心跳”信息
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    //向 Nacos Server 注册服务
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

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

/**
 * register a instance to service with specified instance properties.
 *
 * @param serviceName name of service
 * @param groupName group of service
 * @param instance instance to register
 * @throws NacosException nacos exception
 */
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);

    /** 封装请求参数 Start **/
    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()));
    /** 封装请求参数 End **/

    //向 Nacos Server 发送 HTTP 请求,完成服务的注册
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}

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

/**
 * Add beat information.
 *
 * @param serviceName service name
 * @param beatInfo    beat information
 */
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());
}

线程类 BeatTaskcom.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 {
            //向 Nacos Server 发送“心跳”请求
            JsonNode result = serverProxy.sendBeat(beatInfo, beatReactor);
            this.lightBeatEnabled = 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();
            }
            //Nacos Server 返回该服务不存在,需要重新注册
            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(beatInfo.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: {}, 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 {
                    //启动下一次"心跳"线程,循环执行 BeatTask 线程的 run() 方法,定时发送"心跳"信息
                    executorService.schedule(new BeatTask(beatInfo), nextTime,
                            TimeUnit.MILLISECONDS);
                }
            }
        }
    }
}

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

到这里,服务注册相关的编码和功能讲解就完成了。之前笔者通过项目启动时的一条日志,结合源码分析了服务注册的完整流程。如果读者觉得查看源码比较吃力,那么只需要知道默认情况下在 Spring Cloud Alibaba 2021.x 版本中服务启动后会自动向 Nacos Server 发起注册流程即可。想要了解服务注册背后的原理,建议读者根据本章中整理的源码分析过程和提到的几个具体实现类,自行查看源码并通过 Debug 模式来复盘服务的自动注册流程。

image 2025 04 16 12 23 58 277
Figure 7. 图6-14 服务注册流程简图