生态集成:Nacos
单体应用里,配置跟着代码一起发布,调用方直接写死对端地址。进入多环境、多实例部署后,数据库地址、功能开关、限流阈值必须独立管理,实例上下线也得自动感知——配置中心和服务注册发现就是干这两件事的。
Nacos 把配置管理(Config)和服务管理(Naming)做在了同一个客户端里。本文用一个类对应一个能力:ConfigBootstrap 管配置,RegisterBootstrap 管注册,DiscoverBootstrap 管发现。三个类结构同构,套路一致。

准备依赖与基础配置
Section titled “准备依赖与基础配置”Feat Cloud 没有内置 Nacos starter。 本文直接使用官方 nacos-client,不额外封装。多写几十行胶水代码,换来的是完全透明的链路——连接在哪建立、配置在哪回调、坏配置为什么被拦截,全部一目了然。
引入 Nacos 客户端
Section titled “引入 Nacos 客户端”在已有 Feat Cloud 应用基础上,加上 nacos-client:
<dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>3.2.3</version></dependency>nacos-client 是运行期依赖,feat-cloud-starter 仍然负责编译期生成代码。两者各管各的,互不替代。
在 feat.yml 里声明连接信息
Section titled “在 feat.yml 里声明连接信息”Nacos 服务地址、各能力的 dataId / serviceName / group 这类”环境之间会变、但启动后不变”的参数,放进 feat.yml 最合适。下面这份配置同时承载了配置中心、服务注册、服务发现三组参数:
nacos: serverAddr: 127.0.0.1:8848 config: dataId: config1 group: feat timeout: 1000 register: serviceName: feat-nacos-demo group: DEFAULT_GROUP ip: 127.0.0.1 port: 8082 discover: serviceName: feat-nacos-demo group: DEFAULT_GROUP这些值会在编译期通过 @Value 解析并注入到字段里。@Value 支持 String、int、String[]、List<String> 等少量类型,把 Nacos 连接参数统一用字符串或整数声明最省事。
按环境拆分 Nacos 连接
Section titled “按环境拆分 Nacos 连接”本地开发连本机 Nacos,测试环境连测试集群,生产环境连生产集群。这是典型的 Profile 场景。
nacos: serverAddr: dev-nacos.example.com:8848 config: dataId: config1nacos: serverAddr: prod-nacos.example.com:8848 register: ip: 10.0.1.23 port: 8082启动时通过 feat.profiles.active 选择当前环境:
java -Dfeat.profiles.active=prod -jar yourapp.jar配置中心接入
Section titled “配置中心接入”配置中心最核心的需求不是”能读到配置”,而是”配置变更时应用能安全地用上新值”。ConfigBootstrap 一个类完成连接、拉取、监听和对外暴露,保持最小结构。
package tech.smartboot.feat.nacos;
import com.alibaba.nacos.api.NacosFactory;import com.alibaba.nacos.api.PropertyKeyConst;import com.alibaba.nacos.api.config.ConfigService;import com.alibaba.nacos.api.config.listener.AbstractListener;import tech.smartboot.feat.cloud.FeatCloud;import tech.smartboot.feat.cloud.annotation.Controller;import tech.smartboot.feat.cloud.annotation.PostConstruct;import tech.smartboot.feat.cloud.annotation.PreDestroy;import tech.smartboot.feat.cloud.annotation.RequestMapping;import tech.smartboot.feat.cloud.annotation.Value;
import java.util.Properties;
@Controllerpublic class ConfigBootstrap {
@Value("${nacos.serverAddr}") private String serverAddr;
@Value("${nacos.config.dataId}") private String configDataId;
@Value("${nacos.config.group}") private String configGroup;
@Value("${nacos.config.timeout}") private int configTimeout;
private volatile String config = ""; private ConfigService configService;
private final AbstractListener configListener = new AbstractListener() { @Override public void receiveConfigInfo(String configInfo) { config = configInfo == null ? "" : configInfo; } };
@PostConstruct public void init() throws Exception { Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
try { configService = NacosFactory.createConfigService(properties); // 一次调用完成"拉取初始配置 + 注册监听器",避免拉取与注册之间的空窗期 String initialConfig = configService.getConfigAndSignListener( configDataId, configGroup, configTimeout, configListener ); config = initialConfig == null ? "" : initialConfig; } catch (Exception e) { destroy(); throw e; } }
@PreDestroy public void destroy() throws Exception { if (configService == null) { return; } configService.removeListener(configDataId, configGroup, configListener); configService.shutDown(); configService = null; }
@RequestMapping("/nacos") public String nacos() { return config; }
public static void main(String[] args) { FeatCloud.cloudServer(opt -> opt.setPackages(ConfigBootstrap.class.getName())).listen(8081); }
// setter public void setServerAddr(String serverAddr) { this.serverAddr = serverAddr; } public void setConfigDataId(String configDataId) { this.configDataId = configDataId; } public void setConfigGroup(String configGroup) { this.configGroup = configGroup; } public void setConfigTimeout(int configTimeout) { this.configTimeout = configTimeout; }}这段代码做了四件事:
- 建立连接:用
nacos.serverAddr创建ConfigService - 拉取 + 监听:通过
getConfigAndSignListener一次调用拿到初始值并注册监听器,省去先getConfig再addListener的两步操作,也避免两者之间的空窗期 - 异常自愈:初始化失败时先
destroy()释放已建好的连接,再抛出异常让启动失败暴露问题 - 优雅关闭:停机时先
removeListener再shutDown,断开长连接并清理回调
几个值得注意的细节:
volatile:监听器回调由 Nacos 客户端的长轮询线程触发,config字段被主请求线程读取,加volatile保证可见性。AbstractListener而非裸Listener:Listener接口要求实现getExecutor(),多数场景用不上自定义线程池;AbstractListener给了默认实现,省一个空方法。- dataId / group / timeout 全部走配置:不写死成常量,换环境只改
feat.yml,不用动代码。
启动后:
curl http://localhost:8081/nacos然后去 Nacos 控制台修改 config1 / feat 这个配置的内容,稍等片刻再请求一次,应该能看到新值。
实例启动后把自己登记到 Nacos,消费者才能发现并调用。RegisterBootstrap 的结构和配置中心几乎一致,只是把 ConfigService 换成了 NamingService,把”拉取 + 监听”换成了”注册”。
package tech.smartboot.feat.nacos;
import com.alibaba.nacos.api.NacosFactory;import com.alibaba.nacos.api.PropertyKeyConst;import com.alibaba.nacos.api.naming.NamingService;import tech.smartboot.feat.cloud.FeatCloud;import tech.smartboot.feat.cloud.annotation.Controller;import tech.smartboot.feat.cloud.annotation.PostConstruct;import tech.smartboot.feat.cloud.annotation.PreDestroy;import tech.smartboot.feat.cloud.annotation.RequestMapping;import tech.smartboot.feat.cloud.annotation.Value;
import java.util.Properties;
/** * 服务注册示例:将当前实例注册到 Nacos,供服务消费者发现。 */@Controllerpublic class RegisterBootstrap {
@Value("${nacos.serverAddr}") private String serverAddr;
@Value("${nacos.register.serviceName}") private String serviceName;
@Value("${nacos.register.group}") private String namingGroup;
@Value("${nacos.register.ip}") private String namingIp;
@Value("${nacos.register.port}") private int namingPort;
private NamingService namingService;
@PostConstruct public void init() throws Exception { Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
namingService = NacosFactory.createNamingService(properties); namingService.registerInstance(serviceName, namingGroup, namingIp, namingPort); }
@PreDestroy public void destroy() throws Exception { if (namingService != null) { namingService.shutDown(); namingService = null; } }
@RequestMapping("/nacos/register") public String register() { return "registered: " + serviceName + " -> " + namingIp + ":" + namingPort; }
public static void main(String[] args) { FeatCloud.cloudServer(opt -> opt.setPackages(RegisterBootstrap.class.getName())).listen(8082); }
// setter public void setServerAddr(String serverAddr) { this.serverAddr = serverAddr; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public void setNamingGroup(String namingGroup) { this.namingGroup = namingGroup; } public void setNamingIp(String namingIp) { this.namingIp = namingIp; } public void setNamingPort(int namingPort) { this.namingPort = namingPort; }}registerInstance 把 ip:port 绑定到 serviceName / group 上。停机时 shutDown 会自动反注册,Nacos 通过心跳超时也会摘除实例,两条保险一起兜底。
消费者订阅服务,实例上下线时 Nacos 主动推送变更列表。DiscoverBootstrap 的监听模式和配置中心如出一辙——subscribe 注册回调,NamingEvent 里带最新实例列表。
package tech.smartboot.feat.nacos;
import com.alibaba.nacos.api.NacosFactory;import com.alibaba.nacos.api.PropertyKeyConst;import com.alibaba.nacos.api.naming.NamingService;import com.alibaba.nacos.api.naming.listener.EventListener;import com.alibaba.nacos.api.naming.listener.NamingEvent;import com.alibaba.nacos.api.naming.pojo.Instance;import tech.smartboot.feat.cloud.FeatCloud;import tech.smartboot.feat.cloud.annotation.Controller;import tech.smartboot.feat.cloud.annotation.PostConstruct;import tech.smartboot.feat.cloud.annotation.PreDestroy;import tech.smartboot.feat.cloud.annotation.RequestMapping;import tech.smartboot.feat.cloud.annotation.Value;
import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Properties;
/** * 服务发现示例:订阅 Nacos 中的服务实例变更,并对外暴露当前可用实例列表。 */@Controllerpublic class DiscoverBootstrap {
@Value("${nacos.serverAddr}") private String serverAddr;
@Value("${nacos.discover.serviceName}") private String serviceName;
@Value("${nacos.discover.group}") private String namingGroup;
private volatile List<Instance> instances = Collections.emptyList(); private NamingService namingService;
private final EventListener namingListener = event -> { if (event instanceof NamingEvent) { updateInstances(((NamingEvent) event).getInstances()); } };
@PostConstruct public void init() throws Exception { Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
namingService = NacosFactory.createNamingService(properties); namingService.subscribe(serviceName, namingGroup, namingListener); }
private void updateInstances(List<Instance> changedInstances) { if (changedInstances == null || changedInstances.isEmpty()) { instances = Collections.emptyList(); } else { // 拷贝一份不可变快照,避免回调线程和请求线程并发读写同一个列表 instances = Collections.unmodifiableList(new ArrayList<>(changedInstances)); } }
@PreDestroy public void destroy() throws Exception { if (namingService != null) { namingService.unsubscribe(serviceName, namingGroup, namingListener); namingService.shutDown(); namingService = null; } }
@RequestMapping("/nacos/instances") public List<Instance> instances() { return instances; }
public static void main(String[] args) { FeatCloud.cloudServer(opt -> opt.setPackages(DiscoverBootstrap.class.getName())).listen(8083); }
// setter public void setServerAddr(String serverAddr) { this.serverAddr = serverAddr; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public void setNamingGroup(String namingGroup) { this.namingGroup = namingGroup; }}把 RegisterBootstrap(8082)和 DiscoverBootstrap(8083)都跑起来,先请求发现接口确认能看到注册的实例,然后停掉注册端,过几秒再请求发现接口,实例会自动消失——这就是动态感知。
curl http://localhost:8083/nacos/instances# 停掉 RegisterBootstrap 后等待 10-30 秒,实例列表自动清空curl http://localhost:8083/nacos/instances注意 updateInstances 里做了一次拷贝并包成不可变列表。Nacos 的回调直接传的是内部列表引用,不拷贝的话请求线程遍历时正好被回调线程改写,会抛 ConcurrentModificationException。
生产环境注意事项
Section titled “生产环境注意事项”上线前记住几件事就够了:
- 开鉴权:Nacos 服务端开启鉴权,
namespace/ 用户名 / 口令通过环境变量注入,不要写死在仓库里 - 先校验再刷新:监听到新配置后先检查格式和必填字段,坏配置不要覆盖内存值
- 拷贝回调数据:
receiveConfigInfo/NamingEvent.getInstances()返回的是 Nacos 内部引用,回调线程和请求线程并发访问时务必拷贝快照 - 容器里别用回环 IP:注册的
ip必须是调用方可达地址,容器化部署要注入真实 IP - 优雅关闭:
@PreDestroy里先removeListener/unsubscribe再shutDown,避免停机时还在回调已释放的资源
这几条基本就覆盖了大多数生产问题。
一条可验证的完整链路
Section titled “一条可验证的完整链路”把三个类拼起来,你应该能验证下面三条链路:
配置: Nacos 配置中心 -> ConfigBootstrap -> /nacos注册: 本实例 -> RegisterBootstrap -> Nacos 实例列表发现: Nacos 实例变更 -> DiscoverBootstrap -> /nacos/instances可以用这些命令验证:
curl http://localhost:8081/nacos# 修改 Nacos 里的 config1 / feat,等待 1-3 秒后再请求curl http://localhost:8081/nacos# 注册端运行时,发现端能看到实例curl http://localhost:8083/nacos/instances# 停掉注册端,等待 10-30 秒,实例自动消失curl http://localhost:8083/nacos/instances如果两次请求分别返回修改前后的配置内容、实例列表随上下线动态变化,说明 Nacos 已经完整接入了。
Feat 不包一层 starter,也不重写配置中心和服务发现。Nacos 的 ConfigService、NamingService、Listener、长轮询全是官方原生的东西;Feat 只负责把它接进生命周期:编译期注入、启动时初始化、停机时释放。
三个 Bootstrap 放在一起看,套路完全一致:依赖 + @Value + @PostConstruct + @PreDestroy,中间按官方文档来。 多写几十行胶水代码,换来的是每一行都看得见——连接在哪建立、配置在哪回调、坏配置为什么被拦截、实例为什么被摘除。排查线上问题时,你能立刻定位是 Feat 的生命周期、Nacos 客户端,还是配置内容本身出了问题。
接 Sentinel、Redis、RocketMQ 时,套路一样。