【BlossomConfig】Nacos是如何实现配置文件的读取加载的?

网关项目源码
RPC项目源码
配置中心项目源码

研究一下Nacos是如何实现配置加载与刷新的

Nacos源码的简单的视频讲解
为了验证一下思路是正确的,我引入了一下spring-cloud-starter-bootstrap这个依赖,然后发现,我们的bootstrap配置文件的加载就是通过监听器的方式来实现的。
在这里插入图片描述
这里我们继续深入研究一下nacos是如何完成配置文件加载的,这样子有助于我们后续实现自己的配置中心。
在这里插入图片描述

builder.sources(BootstrapImportSelectorConfiguration.class);

从之前我们学习到的东西我们知道,通过上面的代码,我们又会导入一些新的需要加载的配置。

	List<String> names = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(BootstrapConfiguration.class, classLoader));
		

再nacos的配置中心依赖中,有如下一行配置信息

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration

在这里插入图片描述
所以,nacos就是通过spring的扩展机制也就是自动装配,实现了对自己重要的类的导入。
然后我们来看看这个类都做了些什么。

/*
 * Copyright 2013-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos;

import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator;
import com.alibaba.cloud.nacos.refresh.SmartConfigurationPropertiesRebinder;
import com.alibaba.cloud.nacos.refresh.condition.ConditionalOnNonDefaultBehavior;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author xiaojing
 * @author freeman
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {

   @Bean
   @ConditionalOnMissingBean
   public NacosConfigProperties nacosConfigProperties() {
      return new NacosConfigProperties();
   }

   @Bean
   @ConditionalOnMissingBean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties) {
      return new NacosConfigManager(nacosConfigProperties);
   }

   @Bean
   public NacosPropertySourceLocator nacosPropertySourceLocator(
         NacosConfigManager nacosConfigManager) {
      return new NacosPropertySourceLocator(nacosConfigManager);
   }

   /**
    * Compatible with bootstrap way to start.
    */
   @Bean
   @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
   @ConditionalOnNonDefaultBehavior
   public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
         ConfigurationPropertiesBeans beans) {
      // If using default behavior, not use SmartConfigurationPropertiesRebinder.
      // Minimize te possibility of making mistakes.
      return new SmartConfigurationPropertiesRebinder(beans);
   }

}

按照从上到下加载bean的方式。
我们先看看NacosConfigProperties。
一进来就是重点代码

@PostConstruct
public void init() {
    this.overrideFromEnv();
}

private void overrideFromEnv() {
    if (this.environment != null) {
        if (StringUtils.isEmpty(this.getServerAddr())) {
            String serverAddr = this.environment.resolvePlaceholders("${spring.cloud.nacos.config.server-addr:}");
            if (StringUtils.isEmpty(serverAddr)) {
                serverAddr = this.environment.resolvePlaceholders("${spring.cloud.nacos.server-addr:127.0.0.1:8848}");
            }

            this.setServerAddr(serverAddr);
        }

        if (StringUtils.isEmpty(this.getUsername())) {
            this.setUsername(this.environment.resolvePlaceholders("${spring.cloud.nacos.username:}"));
        }

        if (StringUtils.isEmpty(this.getPassword())) {
            this.setPassword(this.environment.resolvePlaceholders("${spring.cloud.nacos.password:}"));
        }

    }
}

所以我们明白,nacos就是在这里完成了对自己配置信息的加载(因为我们再前面已经通过监听器的机制将配置信息加载到环境配置了,所以这些配置信息我们都可以拿得到)。
nacos的配置信息读取完毕之后,我们可以开始研究一下它是如何操作它的配置中心的。
进入NacosConfigManager类。

public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
   this.nacosConfigProperties = nacosConfigProperties;
   // Compatible with older code in NacosConfigProperties,It will be deleted in the
   // future.
   createConfigService(nacosConfigProperties);
}

/**
 * Compatible with old design,It will be perfected in the future.
 */
static ConfigService createConfigService(
      NacosConfigProperties nacosConfigProperties) {
   if (Objects.isNull(service)) {
      synchronized (NacosConfigManager.class) {
         try {
            if (Objects.isNull(service)) {
               service = NacosFactory.createConfigService(
                     nacosConfigProperties.assembleConfigServiceProperties());
            }
         }
         catch (NacosException e) {
            log.error(e.getMessage());
            throw new NacosConnectionFailureException(
                  nacosConfigProperties.getServerAddr(), e.getMessage(), e);
         }
      }
   }
   return service;
}

不断向下发现就可以知道,nacos使用了工厂模式和反射的方式创建了它的配置中心。

public class ConfigFactory {
    
    /**
     * Create Config.
     *
     * @param properties init param
     * @return ConfigService
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable e) {
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }

好的,配置中心也加载完毕了,下一步就应该是考虑如何从配置中心加载配置文件了。
而下一个刚刚好就是NacosPropertySourceLocator。

public class NacosPropertySourceLocator implements PropertySourceLocator 

不过在这之前我们得先说一下PropertySourceBootstrapConfiguration。
PropertySourceBootstrapConfiguration 是 Spring Cloud 的一个关键类,用于在 Spring Boot 应用启动期间集成外部配置源(如 Nacos)。这个类实现了 ApplicationContextInitializer 接口,这使得它能在 Spring Boot 应用的上下文初始化阶段介入,从而加载和应用外部配置源的配置。
在这里插入图片描述

  1. 作用:
    ○ 当 Spring Boot 应用启动时,PropertySourceBootstrapConfiguration 被用作一个 ApplicationContextInitializer,在 Spring 应用上下文的标准初始化过程之前执行。
    ○ 它负责加载外部配置源(例如 Nacos)的配置,并将这些配置作为新的 PropertySource 添加到 Spring 环境中。这确保了在应用的后续启动过程中,这些外部配置能被应用和访问。
  2. Nacos 的利用方式:
    ○ 对于 Nacos 配置中心,PropertySourceBootstrapConfiguration 会加载 Nacos 的相关配置(如服务地址、数据ID、分组等),然后从 Nacos 服务中拉取相应的配置数据。
    ○ 这个过程通常是通过 NacosPropertySourceLocator 完成的,后者是一个实现了 PropertySourceLocator 接口的类。它负责与 Nacos 通信,获取配置数据,并将其封装为 PropertySource。
  3. 实现 ApplicationContextInitializer 的作用:
    ○ ApplicationContextInitializer 是一个在 Spring 应用上下文初始化之前执行自定义逻辑的扩展点。它允许开发者插入一些在标准 Bean 创建和配置之前的操作。
    ○ 通过实现这个接口,PropertySourceBootstrapConfiguration 能够在 Spring Boot 应用启动的早期阶段介入,确保外部配置源(如 Nacos)中的配置在应用其他部分开始之前就被加载和应用。

而NacosPropertySourceLocator 就是通过对上面的PropertySourceBootstrapConfiguration 类的执行过程中完成的执行的。
在这里插入图片描述

Collection<PropertySource<?>> source = locator.locateCollection(environment);
//执行所有的locator的locate方法。
PropertySource<?> propertySource = locator.locate(environment);

在Spring Cloud中,PropertySourceLocator接口用于实现自定义的属性源定位器,它负责定位和加载配置源(例如从配置服务器或其他外部源)。这个接口通常在实现外部配置中心集成时使用,如在Nacos、Consul或自定义配置服务中。
NacosPropertySourceLocator是Nacos配置中心集成Spring Cloud Config时的具体实现,用于从Nacos服务器加载和定位配置属性。其主要作用是从Nacos中获取配置信息,并将其加入到Spring环境中。下面是locate方法的详细作用解释:

  1. 设置环境:方法接收Environment参数,表示当前Spring应用的环境上下文。nacosConfigProperties.setEnvironment(env)将环境设置到Nacos配置属性中,以便使用其中的配置。
  2. 获取配置服务:configService = nacosConfigManager.getConfigService()获取Nacos的配置服务实例,用于与Nacos服务器通信以获取配置数据。
  3. 检查配置服务实例:如果没有找到配置服务实例,则记录警告并返回null,意味着无法从Nacos加载配置。
  4. 构建Nacos属性源:创建NacosPropertySourceBuilder实例,用于构建和解析从Nacos获取的配置。
  5. 确定数据ID前缀:方法会确定用于从Nacos加载配置的数据ID前缀。这个前缀可能来自Nacos配置属性、应用名称或其他来源。
  6. 创建复合属性源:CompositePropertySource是一个包含多个属性源的容器,方法将从Nacos加载的配置作为其中的属性源加入。
  7. 加载共享配置:loadSharedConfiguration(composite)加载应用的共享配置。
  8. 加载额外配置:loadExtConfiguration(composite)加载应用的额外配置。
  9. 加载应用配置:loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env)根据数据ID前缀和环境信息从Nacos加载应用特定的配置。
  10. 返回复合属性源:最终返回包含所有加载配置的CompositePropertySource实例。

通过这种方式,NacosPropertySourceLocator能够从Nacos配置中心动态地加载和更新配置信息,将其集成到Spring Cloud应用中。这对于实现集中配置管理和动态配置更新非常关键。

因此,我们可以参考Nacos的实现方式,实现PropertySourceLocator。
在Spring Cloud应用启动过程中,Spring框架会自动查找并调用实现了PropertySourceLocator接口的bean的locate方法。

这个过程通常发生在Spring应用上下文初始化的早期阶段,特别是在环境准备(Environment)阶段。Spring Cloud通过这种机制允许外部配置源(如Nacos、Consul或自定义配置服务)在应用启动时提供配置信息。

具体到NacosPropertySourceLocator的情况,它是Spring Cloud Alibaba Nacos配置集成的一部分。当启动Spring Cloud应用时,如果配置了Nacos作为配置源,Spring Cloud框架将自动调用NacosPropertySourceLocator的locate方法来从Nacos服务器获取配置信息,并将其加入到Spring应用的环境中。这样,应用就能使用来自Nacos的配置信息了。

再locate方法的最后有如下这些代码,他们就是负责完成共享、扩展、本项目的配置文件的加载。

loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

在这里插入图片描述
我们只需要复刻Nacos的模式,也就可以完成我们自己的对配置文件的加载。
既然已经知道了配置文件的信息,那么现在就需要通过请求的方式去加载这些配置文件了。

private void loadNacosDataIfPresent(final CompositePropertySource composite,
      final String dataId, final String group, String fileExtension,
      boolean isRefreshable) {
   if (null == dataId || dataId.trim().length() < 1) {
      return;
   }
   if (null == group || group.trim().length() < 1) {
      return;
   }
   NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
         fileExtension, isRefreshable);
   this.addFirstPropertySource(composite, propertySource, false);
}
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
      fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);

最后我们不断debug会进入到NacosConfigService中。
而我们的配置文件最终也就是在这里获取到。

    private final ClientWorker worker;

在这里插入图片描述
上面是第一次项目启动的时候,nacos从配置中心拉取配置。
我们知道,nacos会定期的从配置中心拉取配置,那么这个流程是如何实现的呢?
就是通过上面的ClientWorker实现的。
可以发现其实他就是一个定时任务的线程池。

public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
        final Properties properties) throws NacosException {
    this.configFilterChainManager = configFilterChainManager;
    
    init(properties);
    
    agent = new ConfigRpcTransportClient(properties, serverListManager);
    int count = ThreadUtils.getSuitableThreadCount(THREAD_MULTIPLE);
    ScheduledExecutorService executorService = Executors
            .newScheduledThreadPool(Math.max(count, MIN_THREAD_NUM), r -> {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker");
                t.setDaemon(true);
                return t;
            });
    agent.setExecutor(executorService);
    agent.start();
    
}

最后,会通过如下代码进行对配置中心配置的定时拉取。

@Override
public void startInternal() throws NacosException {
    executor.schedule(() -> {
        while (!executor.isShutdown() && !executor.isTerminated()) {
            try {
                listenExecutebell.poll(5L, TimeUnit.SECONDS);
                if (executor.isShutdown() || executor.isTerminated()) {
                    continue;
                }
                executeConfigListen();
            } catch (Exception e) {
                LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);
    
}

加下来的事情就是通过长连接的方式保持连接,然后定期的判断配置中心和本地的配置文件的区别,如果有更新也需要更新一下本地的配置信息。
这里需要考虑到的是我们不可能每次都全量的拉取所有的配置信息,而是应该增量的拉取和判断。
当配置文件发生变更的时候。会执行refreshContentAndCheck方法。
在 Nacos 配置中心中,refreshContentAndCheck 方法的主要作用是刷新本地缓存的配置内容,并检查其 MD5 值以确认配置是否有更新。这个方法是 Nacos 客户端用来保持本地配置与服务端配置同步的关键部分。
具体来说,方法的功能和流程如下:

  1. 请求配置: 方法使用 getServerConfig 调用 Nacos 服务端,请求指定的配置(由 dataId、group 和 tenant 标识)。3000L 是请求超时时间(毫秒),而 notify 参数决定是否打印日志。
  2. 更新本地缓存: 如果请求成功,将从响应中获取配置内容(getContent),然后更新到本地的 CacheData 对象。这包括配置内容、加密的数据键(如果有)和配置类型(如果提供)。
  3. 日志记录: 如果 notify 为真,会记录一条日志,包含配置的基本信息和内容的摘要。这有助于在配置更新时追踪和调试。
  4. MD5 校验: 调用 cacheData.checkListenerMd5() 方法来检查配置的 MD5 值。如果 MD5 值发生变化(表示配置已更新),将通知所有注册的监听器,以便他们可以响应配置的更改。
    大概流程就是:ClientWorker.cacheData.checkListenerMd5()->CacheData.checkListenerMd5()->
    safeNotifyListener->AbstractSharedListener.innerReceive->NacosContextRefresher初始化
    这里Nacos是通过MD5比对的方式来进行的,具体细节就不说了。

最后,Nacos提供了一个SmartConfigurationPropertiesRebinder。
SmartConfigurationPropertiesRebinder, 是对Spring Cloud的ConfigurationPropertiesRebinder的扩展。它的主要作用是提供一种更灵活的配置属性刷新机制,特别是在处理来自Nacos等外部配置源的动态配置变更时。

  1. fillBeanMap方法的作用:
    ○ fillBeanMap方法负责填充beanMap,这个Map存储了Spring容器中所有标记了@ConfigurationProperties注解的Bean及其名称。
    ○ 它通过反射获取ConfigurationPropertiesBeans实例的beans字段,然后将其内容复制到beanMap中。这样,SmartConfigurationPropertiesRebinder就可以轻松访问和操作这些Bean。
  2. SmartConfigurationPropertiesRebinder的作用:
    ○ 这个类主要用于处理配置属性的动态刷新。在配置源(如Nacos)中的配置项发生变更时,Spring Cloud会发布一个EnvironmentChangeEvent。
    ○ SmartConfigurationPropertiesRebinder监听这个事件,并根据需要重新绑定相关的配置属性Bean。这意味着它可以根据变更的配置项,只刷新那些受影响的Bean,而不是刷新所有的@ConfigurationProperties Bean。
    ○ 它支持两种刷新行为:全局刷新(所有Bean)和特定Bean刷新。这是通过refreshBehavior配置项控制的。特定Bean的刷新可以减少不必要的性能开销,因为它只刷新那些真正受到配置变更影响的Bean。

总结一下,SmartConfigurationPropertiesRebinder的设计目的是为了在配置源发生变化时,提供一种更高效和精准的方式来刷新Spring Boot应用中的配置属性Bean,尤其适用于大型应用,其中配置项众多,频繁的全局刷新可能会导致性能问题。
那么到此为止,我们就已经完全了解了Nacos是如何实现配置信息的加载以及动态刷新本地配置信息的了。

最终分析

既然了解完毕了Nacos是如何实现配置中心的了,那么我们也开始研究一下到底我们自己实现一个配置中心需要做些什么。
其实拆解一下,也就两大核心功能。

  1. 加载bootstrap配置文件,从而实现加载远程配置,并添加远程配置到容器中
  2. 实现动态监听,使得配置变更的时候可以同时刷新本地的配置

同时,对于上面两个点,还可以更加细化。
比如我们还需要考虑拉取配置到本地之后需要对配置进行缓存,同时,我们还需要考虑如何存储Nacos服务端上面的配置信息(比如用数据库)。
我花了一点事件,构思了一下,大概有如下这些关键点。

  1. 加载 Bootstrap 配置文件
    这部分主要涉及启动过程中的配置加载和配置的初始化。
    ● 配置文件解析:
    ○ 支持多种配置文件格式,如 YAML、Properties 等。
    ○ 解析逻辑需要能够处理不同格式的配置文件,并将其转化为内部统一的格式。
    ● 配置源定位:
    ○ 实现 PropertySourceLocator 接口,用于定位并加载配置源。
    ○ 定位远程配置源(如配置中心服务器)并拉取配置数据。
    ● 配置加载顺序管理:
    ○ 确保 bootstrap 配置在应用上下文初始化之前加载。
    ○ 管理不同配置源的优先级,确保优先级高的配置可以覆盖低优先级的配置。
    ● 配置属性绑定:
    ○ 将加载的配置绑定到 Spring 环境中。
    ○ 提供一种机制,允许应用程序通过 @Value、@ConfigurationProperties 等注解访问这些配置。
    ● 环境抽象集成:
    ○ 与 Spring 的环境抽象(Environment)集成。
    ○ 允许通过环境对象访问配置属性。

  2. 实现动态监听
    这部分关注于配置变更的动态监听和相应的更新机制。
    ● 配置变更监听:
    ○ 设计一种机制来监听配置中心的变更事件。
    ○ 支持长轮询、WebSocket 或其他机制来实现实时配置更新。
    ● 配置刷新机制:
    ○ 实现配置变更时的自动刷新机制。
    ○ 刷新影响到的 Spring Bean,确保新的配置能够即时应用。
    ● 条件刷新策略:
    ○ 提供策略来确定哪些配置变更需要触发刷新。
    ○ 避免不必要的配置刷新,减少性能开销。
    ● 配置版本控制和回滚:
    ○ 维护配置的历史版本。
    ○ 在配置错误时提供回滚到旧版本的能力。
    ● 通知和日志记录:
    ○ 当配置更新时,记录日志并发送通知。
    ○ 为了故障排查,记录配置变更的详细信息。
    ● 客户端和服务器端交互:
    ○ 确定客户端(应用程序)与配置中心之间的交互协议。
    ○ 保证数据传输的安全性和高效性。

整理了一下,我们的开发流程大概如下:

  1. Bootstrap配置获取:
    ○ 使用监听器(如ApplicationListener)在Spring Boot启动时读取bootstrap配置文件,并将配置项添加到Environment中。
  2. 配置中心属性类:
    ○ 根据加载的bootstrap配置创建一个配置属性类(如BlossomConfigProperties),使用@ConfigurationProperties注解来绑定配置属性。
  3. 工厂模式创建ConfigFactory:
    ○ 利用工厂模式创建ConfigFactory,该工厂基于BlossomConfigProperties生成ConfigService实例。
  4. 初始化PropertySourceLocator并获取配置:
    ○ 实现PropertySourceLocator,在其中初始化ConfigService,调用其方法获取远程配置信息,并将这些配置添加到PropertySources中。
  5. 处理配置变更和刷新事件:
    ○ 使用Spring Cloud的@RefreshScope或类似机制,确保在配置变更时触发EnvironmentChangeEvent,从而刷新配置注解的属性值(如@Value)。
    ○ 需要考虑@ConfigurationProperties标注的类的动态刷新。
  6. 保持与配置中心的长连接:
    ○ 设计和实现一个长连接机制,定期从配置中心拉取配置更新,以保持客户端和服务器的配置同步。
  7. 本地缓存和数据一致性:
    ○ 设计本地缓存机制(如使用CacheData对象),存储配置项和对应的MD5值,用于检查数据一致性和避免不必要的更新。
  8. 实现Web服务器和客户端:
    ○ 开发Web服务器,提供用户界面和API操作配置。
    ○ 开发客户端,使用长轮询机制不断询问配置中心,以监听配置变更。
  9. 配置变更的同步处理:
    ○ 当检测到配置变更时,同步更新客户端环境,并触发相应的刷新机制

这里我们按照从上到下的方式完成,当然,由于个人时间与能力有限,比较难做的尽善尽美,不过会尽力而为。

了解完毕这些之后,按照我的性格,我会先对我开发可能用到的所有知识进行一套系统完整的学习,确保我在开发的时候遇到问题能快速想到一些解决方法,所以接下来的篇幅我会先用来学习上面Nacos用到的那些技术,比如PropertySourceLocator等等。

什么是配置中心?以及如何实现一个配置中心?

SpringBoot如何实现配置的管控?

SpringCloud项目是如何对bootstrap配置文件进行加载的?

Nacos是如何实现配置文件的读取加载的?

开发配置中心前必须了解的前置知识

配置中心Server和Client端代码的编写

配置中心Core核心功能代码的编写

配置中心源码优化—本地缓存与读写锁

相关推荐

  1. Spring@Bean通过配置文件实现控制???

    2024-04-03 09:32:03       24 阅读
  2. 【spring】外部配置文件

    2024-04-03 09:32:03       13 阅读
  3. Spring Boot 配置文件优先级

    2024-04-03 09:32:03       22 阅读
  4. Vue-Router 如何实现

    2024-04-03 09:32:03       23 阅读
  5. vue如何实现Webpack

    2024-04-03 09:32:03       19 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-03 09:32:03       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-03 09:32:03       5 阅读
  3. 在Django里面运行非项目文件

    2024-04-03 09:32:03       4 阅读
  4. Python语言-面向对象

    2024-04-03 09:32:03       6 阅读

热门阅读

  1. Springboot自动配置原理

    2024-04-03 09:32:03       21 阅读
  2. JVM原理

    2024-04-03 09:32:03       20 阅读
  3. whisper-v3模型部署环境执行

    2024-04-03 09:32:03       21 阅读
  4. HTML/XML转义字符对照

    2024-04-03 09:32:03       22 阅读
  5. CSS世界Ⅰ

    2024-04-03 09:32:03       31 阅读
  6. Github 2024-04-03 C开源项目日报 Top10

    2024-04-03 09:32:03       25 阅读
  7. 【报错】Device /dev/ttyUSB0 is locked.

    2024-04-03 09:32:03       20 阅读
  8. 2.3.16、wc:统计文本

    2024-04-03 09:32:03       18 阅读