Nacos整合之服务注册编码实践
本节正式进入编码环节,使用 Spring Cloud Alibaba
套件整合 Nacos
组件,会实际编写一个服务实例并将其注册至 Nacos
服务中心,重要知识点为代码整合步骤、Nacos
服务中心相关的配置项和服务的自动注册过程。
编写服务代码
前面章节中已经把 Spring Cloud Alibaba
模板项目创建完成,这里可以直接拿过来用,以此为基础进行功能改造。因为是编写与 Nacos
相关的代码,所以这里先把模板项目 spring-cloud-alibaba-demo
的名称改为 spring-cloud-alibaba-nacos-demo
,root
节点的 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-demo
,Java
代码的包名为 ltd.newbee.cloud
。
在该模块的 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">
<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 所示。

在配置文件中添加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.jar
的 spring-configuration-metadata.json
文件中可以查看,如图 6-9 所示。
都是以 “spring.cloud.nacos.discovery.” 开头的配置项,这里节选了部分常用的配置项,如表 6-1 所示。

配置项 | 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
项目了。如果一切正常,则启动成功后可以在控制台看到如下日志输出:

如果未能成功启动,开发人员就需要查看控制台中的日志是否报错,并及时确认问题和修复。
进入 Nacos
控制台,单击 “服务管理” 中的 “服务列表”,可以看到列表中已经存在一条 newbee-cloud-goods-service
的服务信息,如图 6-10 所示,证明服务注册成功。

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

Spring Cloud Alibaba
官方给出的验证方法是直接访问 Nacos Server
的 openAPI
。比如,当前的服务名称是 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 所示。

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

找到本次自动装配的主角: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=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);
//是否为临时服务。默认为临时服务,即默认发送“心跳”信息至 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());
}
线程类 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 {
//向 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
模式来复盘服务的自动注册流程。
