在spring cloud
体系中,有很多的注册中心和配置中心,比如最早的eureka
以及consul
、ZooKeeper
,配置中心有
spring cloud config
及 携程的apollo config
,今天我们要说的是阿里新秀Nacos
来作为配置中心或者注册中心
Nacos是什么
Nacos is committed to help you discover, configure, and manage your microservices. It provides a set of simple and useful features enabling you to realize dynamic service discovery, service configuration, service metadata and traffic management.
Nacos makes it easier and faster to construct, deliver and manage your microservices platform. It is the infrastructure that supports a service-centered modern application architecture with a microservices or cloud-native approach.
Nacos 致力于帮助您发现、配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理.
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台. Nacos 是构建以服务
为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施.
准备
安装Nacos server
动手能力强的小伙伴可以尝试自己编译,需要提前配置Java
及Maven
git clone https://github.com/alibaba/nacos.git
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
ls -al distribution/target/
// change the $version to your actual path
cd distribution/target/nacos-server-$version/nacos/bin
启动Nacos server
Linux/Unix/Mac
启动命令(standalone代表着单机模式运行,非集群模式):
sh startup.sh -m standalone
如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:
bash startup.sh -m standalone
Windows
cmd startup.cmd
你也可以直接双击startup.cmd
运行
如何在spring cloud 中使用
作为配置中心使用
引入依赖
为了方便管理Spring cloud
及nacos
版本,可以提前在父级项目中配置dependencyManagement
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
配置nacos server
application.yml
中添加spring.application.name
及spring.cloud.nacos.server-addr
spring:
application:
name: spring-boot-nacos-config
cloud:
nacos:
server-addr: localhost:127.0.0.1:8848
spring.application.name
是组成nacos 配置管理dataId
字段的一部分spring.cloud.nacos.server-addr
nacos 的地址- 完整的
dataId
在spring cloud nacos
中为${prefix}-${spring.profile.active}.${file-extension}
prefix
默认为spring.application.name
的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置.spring.profile.active
即为当前环境对应的profile
,详情可以参考 Spring Boot文档. 注意:当spring.profile.active
为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
file-exetension
为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension
来配置.目前只支持properties
和yaml
类型,默认为properties
编写启动类
@SpringBootApplication
public class NacosConfigApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConfigApplication.class, args);
}
}
编写Controller
@RestController
@RefreshScope
public class IndexController {
@Value("${nacosConfig}")
private String nacosCongig;
@GetMapping("/config")
public String test(){
return nacosCongig;
}
}
添加配置
- 访问
http://localhost:8848/nacos/
用户名密码nacos
- 点击左侧菜单
配置管理>配置列表
,在右侧点击+
添加配置 - 在输入页填写
Data ID
为我们配置的spring.application.name
- 配置格式选择为
Properties
,在配置内容添加nacosConfig=hello
如果Data ID
填写错误,是无法获取到对应配置的
添加完成后,返回到配置管理,可以看到我们刚刚添加的配置
启动项目
访问http://localhost:8080/config
,此时页面输出hello
,那么恭喜你,大功告成.
作为服务的注册中心使用
添加依赖
添加spring-cloud-starter-alibaba-nacos-discovery
实现服务的注册和发现
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
启动项目
此时,我们到nacos
左侧菜单中点击服务管理>服务列表
,可以看到我们刚刚启动的服务已经成功注册到了列表中
Nacos config 解读
那么,nacos是如何获取到配置在nacos server的配置的呢
查看源码我们可以看到在spring-cloud-starter-alibaba-nacos-config
模块中,nacos添加了com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
的自启动,
其中NacosConfigProperties
就是获取配置的关键,
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
name = {"spring.cloud.nacos.config.enabled"},
matchIfMissing = true
)
public class NacosConfigBootstrapConfiguration {
public NacosConfigBootstrapConfiguration() {
}
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
// 获取配置 实现org.springframework.cloud.bootstrap.config.PropertySourceLocator接口
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
@Order(0) // 设置启动顺序
public class NacosPropertySourceLocator implements PropertySourceLocator {
private static final Logger log = LoggerFactory.getLogger(NacosPropertySourceLocator.class);
private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";
private static final String SEP1 = "-";
private static final String DOT = ".";
private NacosPropertySourceBuilder nacosPropertySourceBuilder;
private NacosConfigProperties nacosConfigProperties;
private NacosConfigManager nacosConfigManager;
/** @deprecated */
@Deprecated
public NacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
}
public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
}
// spring cloud 会执行此方法
public PropertySource<?> locate(Environment env) {
this.nacosConfigProperties.setEnvironment(env);
// 获取配置中心
ConfigService configService = this.nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
} else {
long timeout = (long)this.nacosConfigProperties.getTimeout();
// 获取配置的构造器 有 通过loadNacosData方法获取配置信息
this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
String name = this.nacosConfigProperties.getName();
String dataIdPrefix = this.nacosConfigProperties.getPrefix();
// datdId 前缀,没有就用spring.application.name
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
// 声明nacos配置来源
CompositePropertySource composite = new CompositePropertySource("NACOS");
// 获取公共的配置 通过 spring.cloud.nacos.config.sharedConfigs 配置
this.loadSharedConfiguration(composite);
// 获取扩展的配置 通过 spring.cloud.nacos.config.extensionConfigs 配置
this.loadExtConfiguration(composite);
// 获取单前服务的配置
this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
return composite;
}
}
private void loadSharedConfiguration(CompositePropertySource compositePropertySource) {
List<Config> sharedConfigs = this.nacosConfigProperties.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
this.checkConfiguration(sharedConfigs, "shared-configs");
this.loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<Config> extConfigs = this.nacosConfigProperties.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
this.checkConfiguration(extConfigs, "extension-configs");
this.loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix, NacosConfigProperties properties, Environment environment) {
// 获取配置类型
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
this.loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup, fileExtension, true);
this.loadNacosDataIfPresent(compositePropertySource, dataIdPrefix + "." + fileExtension, nacosGroup, fileExtension, true);
String[] var7 = environment.getActiveProfiles();
int var8 = var7.length;
for(int var9 = 0; var9 < var8; ++var9) {
String profile = var7[var9];
String dataId = dataIdPrefix + "-" + profile + "." + fileExtension;
this.loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, fileExtension, true);
}
}
private void loadNacosConfiguration(final CompositePropertySource composite, List<Config> configs) {
Iterator var3 = configs.iterator();
while(var3.hasNext()) {
Config config = (Config)var3.next();
String dataId = config.getDataId();
String fileExtension = dataId.substring(dataId.lastIndexOf(".") + 1);
this.loadNacosDataIfPresent(composite, dataId, config.getGroup(), fileExtension, config.isRefresh());
}
}
private void checkConfiguration(List<Config> configs, String tips) {
String[] dataIds = new String[configs.size()];
for(int i = 0; i < configs.size(); ++i) {
String dataId = ((Config)configs.get(i)).getDataId();
if (dataId == null || dataId.trim().length() == 0) {
throw new IllegalStateException(String.format("the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId", tips, i));
}
dataIds[i] = dataId;
}
NacosDataParserHandler.getInstance().checkDataId(dataIds);
}
private void loadNacosDataIfPresent(final CompositePropertySource composite, final String dataId, final String group, String fileExtension, boolean isRefreshable) {
if (null != dataId && dataId.trim().length() >= 1) {
if (null != group && group.trim().length() >= 1) {
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group, fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);
}
}
}
// 获取配置,存在就直接取,不存在调用config server获取 关注 this.nacosPropertySourceBuilder.build
private NacosPropertySource loadNacosPropertySource(final String dataId, final String group, String fileExtension, boolean isRefreshable) {
return NacosContextRefresher.getRefreshCount() != 0L && !isRefreshable ? NacosPropertySourceRepository.getNacosPropertySource(dataId, group) : this.nacosPropertySourceBuilder.build(dataId, group, fileExtension, isRefreshable);
}
private void addFirstPropertySource(final CompositePropertySource composite, NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
if (null != nacosPropertySource && null != composite) {
if (!ignoreEmpty || !((Map)nacosPropertySource.getSource()).isEmpty()) {
composite.addFirstPropertySource(nacosPropertySource);
}
}
}
public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
}
}
public class NacosPropertySourceBuilder {
private static final Logger log = LoggerFactory.getLogger(NacosPropertySourceBuilder.class);
private static final Map<String, Object> EMPTY_MAP = new LinkedHashMap();
private ConfigService configService;
private long timeout;
public NacosPropertySourceBuilder(ConfigService configService, long timeout) {
this.configService = configService;
this.timeout = timeout;
}
public long getTimeout() {
return this.timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public ConfigService getConfigService() {
return this.configService;
}
public void setConfigService(ConfigService configService) {
this.configService = configService;
}
NacosPropertySource build(String dataId, String group, String fileExtension, boolean isRefreshable) {
Map<String, Object> p = this.loadNacosData(dataId, group, fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId, p, new Date(), isRefreshable);
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
// 获取配置
private Map<String, Object> loadNacosData(String dataId, String group, String fileExtension) {
String data = null;
try {
// 请求config server 通过dataId和group获取配置
data = this.configService.getConfig(dataId, group, this.timeout);
if (StringUtils.isEmpty(data)) {
log.warn("Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]", dataId, group);
return EMPTY_MAP;
}
if (log.isDebugEnabled()) {
log.debug(String.format("Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId, group, data));
}
// 通过配置类型解析配置到map
Map<String, Object> dataMap = NacosDataParserHandler.getInstance().parseNacosData(data, fileExtension);
return dataMap == null ? EMPTY_MAP : dataMap;
} catch (NacosException var6) {
log.error("get data from Nacos error,dataId:{}, ", dataId, var6);
} catch (Exception var7) {
log.error("parse data from Nacos error,dataId:{},data:{},", new Object[]{dataId, data, var7});
}
return EMPTY_MAP;
}
}
public class NacosConfigService implements ConfigService {
private static final Logger LOGGER = LogUtils.logger(NacosConfigService.class);
private static final long POST_TIMEOUT = 3000L;
private static final String EMPTY = "";
private HttpAgent agent;
private ClientWorker worker;
private String namespace;
private String encode;
private ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager();
// 获取配置
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = this.null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 获取本地文件中的配置
String content = LocalConfigInfoProcessor.getFailover(this.agent.getName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
cr.setContent(content);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
} else {
try {
// ClientWorker 发送请求获取配置
String[] ct = this.worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
} catch (NacosException var9) {
if (403 == var9.getErrCode()) {
throw var9;
} else {
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", new Object[]{this.agent.getName(), dataId, group, tenant, var9.toString()});
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
content = LocalConfigInfoProcessor.getSnapshot(this.agent.getName(), dataId, group, tenant);
cr.setContent(content);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
}
}
}
}
}
获取配置关键
curl get http://config-server:{port}/v1/cs/configs?dataId={dataId}&group={group}&tenant={tenant}
public class ClientWorker {
//tenant 默认为环境变量 tenant.id
// 通过配置 nacos.use.cloud.namespace.parsing=true 如果teannt 为空取 acm.namespace
// 通过配置 nacos.use.cloud.namespace.parsing=fase 如果teannt 为空取 ALIBABA_ALIWARE_NAMESPACE
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
String[] ct = new String[2];
if (StringUtils.isBlank(group)) {
group = "DEFAULT_GROUP";
}
HttpResult result = null;
try {
List<String> params = null;
if (StringUtils.isBlank(tenant)) {
params = new ArrayList(Arrays.asList("dataId", dataId, "group", group));
} else {
params = new ArrayList(Arrays.asList("dataId", dataId, "group", group, "tenant", tenant));
}
// curl get http://config-server:{port}/v1/cs/configs?dataId={dataId}&group={group}&tenant={tenant}
result = this.agent.httpGet("/v1/cs/configs", (List)null, params, this.agent.getEncode(), readTimeout);
} catch (IOException var10) {
String message = String.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", this.agent.getName(), dataId, group, tenant);
LOGGER.error(message, var10);
throw new NacosException(500, var10);
}
switch(result.code) {
case 200:
// 存储文件到本地缓存中
LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, result.content);
ct[0] = result.content;
if (result.headers.containsKey("Config-Type")) {
ct[1] = (String)((List)result.headers.get("Config-Type")).get(0);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
case 403:
LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
throw new NacosException(result.code, result.content);
case 404:
LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)null);
return ct;
case 409:
LOGGER.error("[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
throw new NacosException(409, "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
default:
LOGGER.error("[{}] [sub-server-error] dataId={}, group={}, tenant={}, code={}", new Object[]{this.agent.getName(), dataId, group, tenant, result.code});
throw new NacosException(result.code, "http error, code=" + result.code + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
}
}
}
好啦,今天就说到这里,如有疑问可以评论区留言