跟我学SpringCloud 之 nacos

homeant | 2020-06-27 12:30 浏览99

spring cloud体系中,有很多的注册中心和配置中心,比如最早的eureka以及consulZooKeeper,配置中心有 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官网

准备

安装Nacos server

下载地址

动手能力强的小伙伴可以尝试自己编译,需要提前配置JavaMaven

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 cloudnacos版本,可以提前在父级项目中配置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.namespring.cloud.nacos.server-addr

spring:
  application:
    name: spring-boot-nacos-config
  cloud:
    nacos:
      server-addr: localhost:127.0.0.1:8848
  1. spring.application.name 是组成nacos 配置管理dataId字段的一部分
  2. spring.cloud.nacos.server-addr nacos 的地址
  3. 完整的dataIdspring cloud nacos中为${prefix}-${spring.profile.active}.${file-extension}
    1. prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置.
    2. spring.profile.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档. 注意:当 spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
    3. file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置.目前只支持 propertiesyaml 类型,默认为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;
    }
}

添加配置

  1. 访问http://localhost:8848/nacos/ 用户名密码nacos
  2. 点击左侧菜单配置管理>配置列表,在右侧点击+添加配置
  3. 在输入页填写Data ID为我们配置的spring.application.name
  4. 配置格式选择为Properties,在配置内容添加nacosConfig=hello

如果Data ID填写错误,是无法获取到对应配置的

nacos添加配置

添加完成后,返回到配置管理,可以看到我们刚刚添加的配置

nacos配置管理

启动项目

访问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服务管理

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);
        }
    }
}

好啦,今天就说到这里,如有疑问可以评论区留言

代码地址