springcloudAlibaba之springboot如何加载nacos配置文件

部署运行你感兴趣的模型镜像

配置文件想必大家都很熟悉,无论什么架构 都离不开配置,虽然spring boot已经大大简化了配置,但如果服务很多 环境也好几个,管理配置起来还是很麻烦,并且每次改完配置都需要重启服务,nacos config出现就解决了这些问题,它把配置统一放到服务进行管理,客户端这边进行有需要的获取,可以实时对配置进行修改和发布

在这里解释下namespace和group的概念,namespace可以用来解决不同环境的问题,group是来管理配置分组的,它们的关系如下图
在这里插入图片描述

spring boot启动容器如何加载nacos config配置文件

在这里插入图片描述
这个配置作用是spring在启动之间准备上下文时会启用这个配置 来导入nacos相关配置文件,为后续容器启动做准备

来看NacosConfigBootstrapConfiguration这个配置类

在这里插入图片描述
NacosConfigProperties:对应我们上面在bootstrap.properties中对应的配置信息

NacosConfigManager: 持有NacosConfigProperties和ConfigService,ConfigService用来查询 发布配置的相关接口

NacosPropertySourceLocator:它实现了PropertySourceLocator ,spring boot启动时调用PropertySourceLocator.locate(env)用来加载配置信息,下面来看相关源码

/******************************************NacosPropertySourceLocator******************************************/
public PropertySource<?> locate(Environment env) {
    ConfigService configService = this.nacosConfigProperties.configServiceInstance();
    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();
        this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
        String name = this.nacosConfigProperties.getName();
        String dataIdPrefix = this.nacosConfigProperties.getPrefix();
        if (StringUtils.isEmpty(dataIdPrefix)) {
            dataIdPrefix = name;
        }

        if (StringUtils.isEmpty(dataIdPrefix)) {
            dataIdPrefix = env.getProperty("spring.application.name");
        }

        CompositePropertySource composite = new CompositePropertySource("NACOS");
        // 加载共享的配置文件 不同指定分组 默认DEFAULT_GROUP,对应配置spring.cloud.nacos.config.sharedDataids=shared_1.properties,shared_2.properties
        this.loadSharedConfiguration(composite);
        // 对应spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties的配置
        this.loadExtConfiguration(composite);
        // 加载当前应用配置
        this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
        return composite;
    }
}

// 看一个加载实现即可 流程都差不多 具体实现在NacosPropertySourceBuilder.loadNacosData()方法完成
/******************************************具体实现在NacosPropertySourceBuilder******************************************/
private Properties loadNacosData(String dataId, String group, String fileExtension) {
        String data = null;

        try {
            // 向nacos server拉取配置文件
            data = this.configService.getConfig(dataId, group, this.timeout);
            if (!StringUtils.isEmpty(data)) {
                log.info(String.format("Loading nacos data, dataId: '%s', group: '%s'", dataId, group));
                // spring boot配置当然只支持properties和yaml文件格式
                if (fileExtension.equalsIgnoreCase("properties")) {
                    Properties properties = new Properties();
                    properties.load(new StringReader(data));
                    return properties;
                }

                if (fileExtension.equalsIgnoreCase("yaml") || fileExtension.equalsIgnoreCase("yml")) {
                    YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
                    yamlFactory.setResources(new Resource[]{new ByteArrayResource(data.getBytes())});
                    return yamlFactory.getObject();
                }
            }
        } 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_PROPERTIES;
    }

至此我们在nacos上配置的properties和yaml文件都载入到spring配置文件中来了,后面可通过context.Environment.getProperty(propertyName)来获取相关配置信息

配置如何随spring boot加载进来我们说完了,接来下来看修改完配置后如何实时刷新

nacos config动态刷新

当nacos config更新后,根据配置中的refresh属性来判断是否刷新配置,配置如下

spring.cloud.nacos.config.ext-config[0].refresh=true
首先sprin.factories 配置了EnableAutoConfiguration=NacosConfigAutoConfiguration,NacosConfigAutoConfiguration配置类会注入一个NacosContextRefresher,它首先监听了ApplicationReadyEvent,然后注册一个nacos listener用来监听nacos config配置修改后发布一个spring refreshEvent用来刷新配置和应用

public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware

public void onApplicationEvent(ApplicationReadyEvent event) {
    // 只注册一次
    if (this.ready.compareAndSet(false, true)) {
        this.registerNacosListenersForApplications();
    }
}
    
private void registerNacosListenersForApplications() {
    if (this.refreshProperties.isEnabled()) {
        Iterator var1 = NacosPropertySourceRepository.getAll().iterator();
        while(var1.hasNext()) {
            NacosPropertySource nacosPropertySource = (NacosPropertySource)var1.next();
            // 对应刚才所说的配置 需要配置文件是否需要刷新
            if (nacosPropertySource.isRefreshable()) {
                String dataId = nacosPropertySource.getDataId();
                // 注册nacos监听器
                this.registerNacosListener(nacosPropertySource.getGroup(), dataId);
            }
        }
    }

}
    
private void registerNacosListener(final String group, final String dataId) {
    Listener listener = (Listener)this.listenerMap.computeIfAbsent(dataId, (i) -> {
        return new Listener() {
            public void receiveConfigInfo(String configInfo) {
                NacosContextRefresher.refreshCountIncrement();
                String md5 = "";
                if (!StringUtils.isEmpty(configInfo)) {
                    try {
                        MessageDigest md = MessageDigest.getInstance("MD5");
                        md5 = (new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))).toString(16);
                    } catch (UnsupportedEncodingException | NoSuchAlgorithmException var4) {
                        NacosContextRefresher.log.warn("[Nacos] unable to get md5 for dataId: " + dataId, var4);
                    }
                }
                // 添加刷新记录
                NacosContextRefresher.this.refreshHistory.add(dataId, md5);
                // 发布一个spring refreshEvent事件 对应监听器为RefreshEventListener 该监听器会完成配置的更新应用
                NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
                if (NacosContextRefresher.log.isDebugEnabled()) {
                    NacosContextRefresher.log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
                }

            }
            public Executor getExecutor() {
                return null;
            }
        };
    });

    try {
        this.configService.addListener(dataId, group, listener);
    } catch (NacosException var5) {
        var5.printStackTrace();
    }

}

我们说完了nacos config动态刷新,那么肯定有对应的动态监听,nacos config会监听nacos server上配置的更新状态

nacos config动态监听

一般来说客户端和服务端数据交互无非就两种方式

pull:客户端主动从服务器拉取数据

push: 由服务端主动向客户端推送数据

这两种模式优缺点各不一样,pull模式需要考虑的是什么时候向服务端拉取数据 可能会存在数据延迟问题,而push模式需要客户端和服务端维护一个长连接 如果客户端较多会给服务端造成压力 但它的实时性会更好

nacos采用的是pull模式,但它作了优化 可以看做是pull+push,客户端会轮询向服务端发出一个长连接请求,这个长连接最多30s就会超时,服务端收到客户端的请求会先判断当前是否有配置更新,有则立即返回

如果没有服务端会将这个请求拿住“hold”29.5s加入队列,最后0.5s再检测配置文件无论有没有更新都进行正常返回,但等待的29.5s期间有配置更新可以提前结束并返回,下面会在源码中讲解具体怎么处理的

nacos client处理

动态监听的发起是在ConfigService的实现类NacosConfigService的构造方法中,它是对外nacos config api接口,在之前加载配置文件和NacosContextRefresher构造方法中都会获取或创建
在这里插入图片描述
在这里插入图片描述
这里都会先判断是否已经创建了ConfigServer,没有则实例化一个NacosConfigService,来看它的构造函数

/***************************************** NacosConfigService *****************************************/
public NacosConfigService(Properties properties) throws NacosException {
    String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
    if (StringUtils.isBlank(encodeTmp)) {
        encode = Constants.ENCODE;
    } else {
        encode = encodeTmp.trim();
    }
    initNamespace(properties);
    // 用来向nacos server发起请求的代理,这里用到了装饰模式
    agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    agent.start();
    // 客户端的一个工作类,agent作为它的构造传参 可猜想到里面肯定会做一些远程调用
    worker = new ClientWorker(agent, configFilterChainManager, properties);
}

/***************************************** ClientWorker *****************************************/
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
    this.agent = agent;
    this.configFilterChainManager = configFilterChainManager;

    // Initialize the timeout parameter

    init(properties);
    // 这个线程池只有一个核心线程 用来执行checkConfigInfo()方法
    executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
            t.setDaemon(true);
            return t;
        }
    });
    // 其它需要执行线程的地方都交给这个线程池来处理
    executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
            t.setDaemon(true);
            return t;
        }
    });
    
    // 执行一个调用checkConfigInfo()方法的周期性任务,每10ms执行一次,首次执行延迟1ms后执行
    executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                checkConfigInfo();
            } catch (Throwable e) {
                LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
            }
        }
    }, 1L, 10L, TimeUnit.MILLISECONDS);
}

NacosConfigService构造方法主要创建一个agent 它是用来向nacos server发出请求的,然后又创建了一个clientwoker,它的构造方法创建了两个线程池,第一个线程池只有一个核心线程,它会执行一个周期性任务只用来调用checkconfiginfo()方法,第二个线程是后续由需要执行线程的地方都交给它来执行,下面重点来看checkconfiginfo()方法

public void checkConfigInfo() {
    // 分任务
    int listenerSize = cacheMap.get().size();
    // 向上取整为批数
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}
AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(    new HashMap<String, CacheData>());

cacheMap:缓存着需要刷新的配置,它是在调用ConfigService 添加监听器方式时会放入,可以自定义监听配置刷新

// 添加一个config监听器,用来监听dataId为ErrorCode,group为DEFAULT_GROUP的config
configService.addListener("ErrorCode","DEFAULT_GROUP",new Listener() {
    @Override
    public Executor getExecutor() {
        return null;
    }

    @Override
    public void receiveConfigInfo(String s) { //当配置更新时会调用监听器该方法
        Map<String, Map<String, String>> map = JSON.parseObject(s, Map.class);
        // 根据自己的业务需要来处理
    }
});

这里采用了一个策略:将cacheMap中的数量以3000分一个组,分别创建一个LongPollingRunnable用来监听配置更新,这个LongPollingRunnable就是我们之前所说的长连接任务,来看这个长连接任务

class LongPollingRunnable implements Runnable {
    private int taskId;

    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {

        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // check failover config
            for (CacheData cacheData : cacheMap.get().values()) {
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        // 1、检查本地配置
                        checkLocalConfig(cacheData);
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // 2、向nacos server发出一个长连接 30s超时,返回nacos server有更新过的dataIds
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);

            for (String groupKey : changedGroupKeys) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0];
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    // 3、向nacos server请求获取config最新内容
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                } 
            }
            // 4、对有变化的config调用对应监听器去处理
            for (CacheData cacheData : cacheDatas) {
                if (!cacheData.isInitializing() || inInitializingCacheList
                    .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();
            // 继续轮询
            executorService.execute(this);
        } catch (Throwable e) {
            // 发生异常延迟执行
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

这个长轮询主要做了4个步骤

检查本地配置,如果存在本地配置,并且与缓存中的本地配置版本不一样,把本地配置内容更新到缓存,并触发事件,这块源码比较简单,读者跟到源码一读编制
向nacos server发出一个长连接,30s超时,nacos server会返回有变化的dataIds
根据变化的dataId,从服务端拉取最新的配置内容然后更新到缓存中
对有变化的配置 触发事件监听器来处理
讲完了nacos client处理流程,再来看服务端这边怎么处理这个长连接的

nacos server处理

服务端长连接接口是/config/listener,对应源码包为config

/****************************************** ConfigController ******************************************/
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
    String probeModify = request.getParameter("Listening-Configs");
    if (StringUtils.isBlank(probeModify)) {
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
    // 需要检查更新的config信息
    Map<String, String> clientMd5Map;
    try {
        clientMd5Map = MD5Util.getClientMd5Map(probeModify);
    } catch (Throwable e) {
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    // 长连接处理
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

/****************************************** ConfigServletInner ******************************************/
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
            Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
    
    // 判断是否支持长轮询
    if (LongPollingService.isSupportLongPolling(request)) {
        // 长轮询处理
        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
        return HttpServletResponse.SC_OK + "";
    }
    
    // 不支持长轮询,直接与当前配置作比较,返回有变更的配置
    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
    
    // Compatible with short polling result.
    String oldResult = MD5Util.compareMd5OldResult(changedGroups);
    String newResult = MD5Util.compareMd5ResultString(changedGroups);
    
    /*
    * 省略
    * 会响应变更的配置信息
    */
    return HttpServletResponse.SC_OK + "";
}

/****************************************** LongPollingService ******************************************/
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
            int probeRequestSize) {
        
    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
    String tag = req.getHeader("Vipserver-Tag");
    
    // 服务端这边最多处理时长29.5s,需要留0.5s来返回,以免客户端那边超时
    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
    // Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
    if (isFixedPolling()) {
        timeout = Math.max(10000, getFixedPollingInterval());
        // Do nothing but set fix polling timeout.
    } else {
        // 不支持长轮询 本地对比返回
        long start = System.currentTimeMillis();
        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
        if (changedGroups.size() > 0) {
            generateResponse(req, rsp, changedGroups);
            // log....
            return;
        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
            // log....
            return;
        }
    }
    String ip = RequestUtil.getRemoteIp(req);
    
    // 将http响应交给异步线程,返回一个异步响应上下文, 当配置更新后可以主动调用及时返回,不用非等待29.5s
    final AsyncContext asyncContext = req.startAsync();
    
    // AsyncContext.setTimeout() is incorrect, Control by oneself
    asyncContext.setTimeout(0L);
    // 执行客户端长连接任务,
    ConfigExecutor.executeLongPolling(
            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

/****************************************** ClientLongPolling ******************************************/
class ClientLongPolling implements Runnable {
        
    @Override
    public void run() {
        // 提交一个任务,延迟29.5s执行
        asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
            @Override
            public void run() {
                try {
                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                    
                    // Delete subsciber's relations.
                    allSubs.remove(ClientLongPolling.this);
                    
                    if (isFixedPolling()) {
                        // 检查变更配置 并相应
                        List<String> changedGroups = MD5Util
                                .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                        (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                        if (changedGroups.size() > 0) {
                            sendResponse(changedGroups);
                        } else {
                            sendResponse(null);
                        }
                    } else {
                        sendResponse(null);
                    }
                } catch (Throwable t) {
                    LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                }
                
            }
            
        }, timeoutTime, TimeUnit.MILLISECONDS);
        
        allSubs.add(this);
    }
}
final Queue<ClientLongPolling> allSubs

上面大部分地方都比较好懂,主要解释下ClientLongPolling作用,它首先会提交一个任务,无论配置有没有更新 最终都会进行响应,延迟29.5s执行,然后会把自己添加到一个队列中,之前说过,服务端这边配置有更新后 会找出正在等待配置更新的长连接任务,提前结束这个任务并返回
来看这一步是怎么处理的

public LongPollingService() {
    allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
    
    ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
    
    // Register LocalDataChangeEvent to NotifyCenter.
    NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
    
    // Register A Subscriber to subscribe LocalDataChangeEvent.
    NotifyCenter.registerSubscriber(new Subscriber() {
        
        @Override
        public void onEvent(Event event) {
            if (isFixedPolling()) {
                // Ignore.
            } else {
                if (event instanceof LocalDataChangeEvent) {
                    LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                    ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                }
            }
        }
        
        @Override
        public Class<? extends Event> subscribeType() {
            return LocalDataChangeEvent.class;
        }
    });
    
}

class DataChangeTask implements Runnable {
        
    @Override
    public void run() {
        try {
            ConfigCacheService.getContentBetaMd5(groupKey);
            // 找出等在该配置的长连接,然后进行提前返回
            for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                ClientLongPolling clientSub = iter.next();
                if (clientSub.clientMd5Map.containsKey(groupKey)) {
                    // If published tag is not in the beta list, then it skipped.
                    if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                        continue;
                    }
                    
                    // If published tag is not in the tag list, then it skipped.
                    if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                        continue;
                    }
                    
                    getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                    iter.remove(); // Delete subscribers' relationships.
                    clientSub.sendResponse(Arrays.asList(groupKey));
                }
            }
        } catch (Throwable t) {
            LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
        }
    }
}

LongPollingService构造函数中,会注册一个订阅,用来监听LocalDataChangeEvent,当发生该事件时,会执行一个数据变更任务,这个任务就是找出等在配置的长连接,提前返回

我们在nacos控制台修改一个配置文件进行发布,会调用ConfigController.publishConfig接口,但这个接口发布的是ConfigDataChangeEvent事件,大意了。。。LocalDataChangeEvent事件发布在ConfigCacheService,这里怎么调用的我就不深追,留给有兴趣的读者

至此nacos config动态监听、刷新就串联起来了,nacos的相关源码都比较好理解,跟着源码追进去就一目了然了

本片文章搬运博客园博主 努力工作的小码农

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

<think>我们正在讨论如何在SpringBoot应用中切换Nacos配置环境。根据引用内容,我们可以总结出几个关键点:1.**配置Nacos服务器地址**:在`application.properties`或`application.yml`中配置`spring.cloud.nacos.config.server-addr`(引用[1])。2.**启用服务发现**:在主启动类上添加`@EnableDiscoveryClient`注解(引用[2])。3.**多环境配置**:使用`bootstrap.properties`或`bootstrap.yml`来指定环境(引用[3])。在Spring Boot2.4之前,通常使用`bootstrap`配置文件。从2.4开始,需要引入`spring-cloud-starter-bootstrap`依赖才能继续使用`bootstrap`配置。 4. **命名空间的使用**:通过命名空间(namespace)来隔离不同环境(如DEV/SIT/UAT/PRD)。在配置中设置`spring.cloud.nacos.config.namespace`,其值为Nacos中对应环境的命名空间ID(引用[3][4])。 5. **动态切换环境**:可以通过环境变量或配置文件来动态设置命名空间,例如:`spring.cloud.nacos.config.namespace=${NACOS_CONFIG_NS}`(引用[4])。 ###解决方案步骤####1.添加必要的依赖确保`pom.xml`中包含以下依赖:```xml<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> <!--如果使用SpringBoot2.4及以上版本,并且想使用bootstrap配置,需要添加此依赖 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency> ```####2.配置`bootstrap.properties`/`bootstrap.yml`在`src/main/resources`目录下创建`bootstrap.properties`(或`bootstrap.yml`)文件,配置Nacos服务器地址和当前激活的环境(profile): ```properties#指定环境,例如:dev,sit,uat, prodspring.profiles.active=dev# Nacos配置中心地址spring.cloud.nacos.config.server-addr=your-nacos-server-address``` ####3.配置命名空间在Nacos控制台中创建不同环境的命名空间,并记录其命名空间ID(一个字符串,如"dev-namespace-id")。然后在`bootstrap.properties`中配置命名空间:```properties#设置命名空间,值为Nacos中对应环境的命名空间IDspring.cloud.nacos.config.namespace=dev-namespace-id```####4.动态配置命名空间为了更灵活地切换环境,可以使用环境变量来设置命名空间: ```propertiesspring.cloud.nacos.config.namespace=${NACOS_NAMESPACE:dev-namespace-id}``` 这里`${NACOS_NAMESPACE:dev-namespace-id}`表示优先使用环境变量`NACOS_NAMESPACE`,如果未设置则使用默认值`dev-namespace-id`。####5.主启动类添加注解确保主启动类上添加了`@EnableDiscoveryClient`注解(引用[2]):```java@SpringBootApplication@EnableDiscoveryClientpublicclass Application{public staticvoid main(String[]args){SpringApplication.run(Application.class, args);} }``` ####6.重启应用并验证重启SpringBoot应用,检查是否从正确的Nacos命名空间加载配置。可以通过查看启动日志或访问应用的/actuator/configprops端点来验证。 ###注意事项1. **Spring Boot版本兼容性**:-如果使用Spring Boot2.4及以上版本,默认不支持`bootstrap`配置,需要添加`spring-cloud-starter-bootstrap`依赖(引用[3])。-或者,可以将配置放在`application.properties`中,但要注意配置的加载顺序。 2. **命名空间ID**:-确保配置的命名空间ID与Nacos控制台中的完全一致(注意不是命名空间名称,而是ID)。命名空间ID可以在Nacos控制台的“命名空间”页面找到。 3. **环境变量设置**:-在运行应用时,可以通过设置环境变量来覆盖默认的命名空间,例如:```bashexportNACOS_NAMESPACE=sit-namespace-idjava-jaryour-app.jar```-或者在Docker容器中设置环境变量。 4. **配置刷新**:-如果配置未生效,可以尝试手动刷新Nacos客户端的配置缓存(引用[4]),或者使用`@RefreshScope`注解来动态刷新配置。 5. **多环境配置**:-除了命名空间,还可以使用`spring.profiles.active`来指定环境,这样Nacos加载对应环境下的配置文件(如`application-dev.properties`)。 6. **日志排查**:-启动时注意查看日志,搜索“Nacos”相关日志,确认连接的服务端地址、命名空间以及加载的配置。7.**网络连通性**:-确保应用服务器能够访问Nacos配置中心地址。 ###常见问题处理-**问题:配置未生效**-检查`bootstrap`配置文件是否正确加载(查看启动日志)。-检查命名空间ID是否配置正确(注意不是名称)。-在Nacos控制台检查对应命名空间下是否有正确的配置文件(Data ID通常为`${spring.application.name}.properties`或`${spring.application.name}-${profile}.properties`)。- **问题:无法连接到Nacos**-检查`server-addr`配置是否正确,以及网络是否连通。###总结切换Nacos配置环境的核心在于正确配置命名空间(namespace)和激活的环境(profile)。通过`bootstrap`配置文件或环境变量动态设置,结合Nacos控制台的多命名空间管理,可以实现环境隔离和动态切换。 §§相关问题§§1.如何在SpringBoot2.4及以上版本使用bootstrap配置? 2. Nacos配置中心如何管理不同环境的配置? 3.如何通过KubernetesConfigMap实现类似Nacos的环境隔离? 4.在SpringCloudAlibaba中,如何实现配置的动态刷新?</think>### Spring Boot 应用切换 Nacos 配置环境的完整指南 #### 一、核心配置方法 1. **命名空间隔离**(关键步骤) 在 Nacos 控制台创建不同环境的命名空间(如 DEV/SIT/UAT),获取命名空间 ID: ```properties # application.properties spring.cloud.nacos.config.namespace=${NACOS_NAMESPACE_ID} # 引用[3][4] ``` 2. **动态环境切换** - 通过环境变量注入命名空间: ```bash java -jar app.jar --NACOS_NAMESPACE_ID=dev-namespace-id ``` - 或在 `bootstrap.properties` 中指定: ```properties spring.profiles.active=dev # 引用[3] ``` 3. **多环境配置结构** Nacos 配置文件建议采用标准命名: ``` ${spring.application.name}-${profile}.properties ``` 例如:`order-service-dev.properties` #### 二、必要依赖和注解 1. **启动类注解** 必须添加服务发现注解: ```java @SpringBootApplication @EnableDiscoveryClient // 引用[2] public class Application { ... } ``` 2. **Maven 依赖** 确保包含最新 Nacos 配置依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2022.0.0.0</version> </dependency> ``` #### 三、完整配置示例 ```yaml # bootstrap.yml spring: application: name: user-service profiles: active: @activatedProperties@ # Maven过滤占位符 cloud: nacos: config: server-addr: 192.168.1.100:8848 namespace: ${NACOS_NS:dev-default-id} # 环境变量优先 file-extension: yaml discovery: namespace: ${NACOS_NS} # 与服务发现一致 ``` #### 四、切换环境操作步骤 1. **本地开发**: ```bash export NACOS_NS=dev-namespace-id mvn spring-boot:run ``` 2. **生产部署**: ```bash java -jar app.jar --NACOS_NS=prod-namespace-id ``` 3. **Docker 环境**: ```dockerfile ENV NACOS_NS=uat-namespace-id CMD ["java","-jar","/app.jar"] ``` #### 五、注意事项和故障排查 1. **配置未生效**: - 检查 `bootstrap` 配置优先级高于 `application` [^3] - 确认 Nacos 控制台配置已发布 - 查看启动日志:`Loading nacos data, dataId: user-service-dev.yaml` 2. **版本兼容性**: | Spring Boot 版本 | 配置方式 | 额外依赖 | |------------------|-------------------|------------------------| | < 2.4.x | bootstrap.yml | 无需额外依赖 | | ≥ 2.4.x | application.yml | spring-cloud-starter-bootstrap | 3. **安全建议**: - 禁止将生产命名空间 ID 写入代码 - 通过 CI/CD 管道注入环境变量[^4] - 使用 Nacos 的配置加密功能 4. **动态刷新**: ```java @RestController @RefreshScope // 配置变更自动生效 public class ConfigController { @Value("${custom.config}") private String config; } ``` #### 六、最佳实践 1. **环境隔离策略**: ```mermaid graph LR A[Nacos 命名空间] --> B[DEV] A --> C[SIT] A --> D[UAT] A --> E[PROD] B --> F[配置组1] B --> G[配置组2] ``` 2. **多环境切换流程**: ```mermaid sequenceDiagram 开发者->>+CI/CD: 提交代码 CI/CD->>+Nacos: 获取PROD命名空间ID CI/CD->>+K8s: 注入NACOS_NS环境变量 K8s->>+Spring Boot: 启动应用 Spring Boot->>+Nacos: 请求PROD配置 Nacos-->>-Spring Boot: 返回配置 Spring Boot-->>-用户: 服务就绪 ``` > 关键提示:**当切换环境后配置未加载时,检查命名空间ID是否包含非法字符(如@符号),需使用纯字符串格式**[^1][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值