【Nacos无压力源码领读】(三) Nacos 配置中心与热更新原理详解 敢说全网最细

本文将从 Nacos 配置中心的基本使用入手, 详细介绍 Nacos 客户端发布配置, 拉取配置, 订阅配置的过程以及服务器对应的处理过程;

配置订阅以及热更新原理相关的部分, 我看了主流的博客网站, 绝对没有比这更详细的讲解;

如果在阅读过程中对文中提到的 SpringBoot 启动过程以及扩展机制不太了解, 或者出现了没见过的词, 在这篇文章 SpringBoot启动流程与配置类处理机制详解, 附源码与思维导图 中都能找到答案, 强烈建议学习后再来读本文;

对SpringBoot 属性加载机制不了解的可以看这里 SpringBoot 属性加载机制

Nacos配置中心

基本使用

一个大型项目, 服务数量会非常多, 每个服务都需要进行配置, 这会导致: 公共的一些配置需要修改时, 需要修改大量服务的配置文件; 并且需要重新启动涉及的所有服务;

Nacos就提供了作为配置中心的功能; 将配置(不仅是公共的, 服务独有的配置也可以保存到Nacos)保存在Nacos服务器中, 实现以中心化 (都在Nacos里)、外部化 (配置独立于服务之外保存) 和动态化 (动态化即可以实时刷新配置) 的方式管理配置。

基本原理

将配置以键值对的形式保存到Nacos中;

服务启动时, 从 Nacos 读取配置, 生成 PropertySource 对象, 保存到 Spring 容器中, 可以使用 @Value注解 等方式进行注入;

配置模型

为了更好地管理配置, 使得一个Nacos配置中心能支持多个应用, 多个服务, Nacos划分了几个不同的维度来组织保存的配置:

命名空间 Namespace

  • 用于用户粒度的配置隔离, 每一个命名空间都有一个唯一的ID与之对应, 默认情况下使用 public 命名空间;
  • 创建一个命名空间时, Nacos 会自动分配一个唯一的ID, 不需要用户指定;

分组 Group

  • 每个分组都有一个唯一的组名, 默认使用DEFAULT-GROUP分组;
  • 创建分组时, 由用户指定分组名称;

配置集 Data ID

  • 配置集是一组配置项的集合, 类比一个服务的一个配置文件;
  • 每个配置集都有一个唯一的 Data ID, 需要用户在创建配置集时手动指定;
  • SpringBoot项目启动时, 会自动生成一个配置集的DataID, 并从Nacos下载对应的配置;
  • 生成的DataID由三部分构成
${prefix}-${spring.profiles.active}.${file-extension}
  • 第一个字段 默认取spring.application.name的值, 也可以通过配置项 spring.cloud.nacos.config.prefix 来配置。

  • 第二个字段, 取spring.profiles.active的值, 并且在最前面拼接一个短横杠, 如果该属性没有配置, 这个字段为空, 也没有短横杠;

    spring.profiles.active用于指定激活的 Spring 配置文件。application.yml文件始终有效, 可以选择是否激活其它的配置文件; 可以根据不同的环境(例如开发、测试、生产等)来加载不同的配置,从而实现环境隔离和配置管理。

  • 第三个字段通过spring.cloud.nacos.config.file-extension属性配置,可以是 propertiesyaml;

  • 例如一个名称为 student-service 的服务, 激活了dev 配置文件, 配置的后缀是 yaml, 那么生成的DataID是

    student-service-dev.yaml;

配置项 Key - Value

  • 一个配置项就对应一个配置, 将配置的参数名与取值以键值对的形式保存;

使用Nacos配置中心

导包

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

在单个服务中, 添加配置文件bootstrap.yml, 将和 Nacos-Config 相关的配置放到bootstrap.yml中;

  • bootstrap.yml 文件中的配置在应用启动的早期阶段 (引导阶段) 就会被加载;
  • application.yml 文件在 PropertySourceList 中的位置在 bootstrap.yml 之前,优先级更高, 因此可以覆盖 bootstrap.yml 中的配置;
  • Nacos 配置中心的相关配置需要在应用启动的早期阶段加载,以便应用能够尽早连接到 Nacos 服务器获取配置。
spring:application:# 生成DataID需要name: facade-serviceprofiles:# 生成DataID需要active: devcloud:nacos:config:# 服务器地址server-addr: localhost:8848# 生成DataID需要file-extension: yaml# 指定groupgroup: DEFAULT_GROUP# 指定namespace, 值应该取命名空间对应的ID, 而不是空间名namespace: eaf7318f-2016-4fb2-a37e-3d528087e550

对 Nacos 配置集中的配置, 可以直接注入并使用; 此时还无法实现配置的热更新; 修改Nacos中保存的配置后, 不会在服务中立即生效, 需要重启服务;

@Value("${nacos.test.config0}")
String nacosTestConfig0;@GetMapping("/facade/nacosconfig")
public String nacosConfig() {return nacosTestConfig0;
}

在使用了配置的类上添加@RefreshScope注解开启热更新;

开启热更新后, 无需重启服务, 即可实现配置更新;

@RefreshScope
public class FacadeController {@Value("${nacos.test.config0}")String nacosTestConfig0;@GetMapping("/facade/nacosconfig")public String nacosConfig() {return nacosTestConfig0;}
}

持久化

Derby 是一个基于JAVA开发关系型数据库, 是可嵌入的。应用程序可以将 Derby 嵌入应用程序进程中,从而无需管理单独的数据库进程或服务。

我们在Nacos服务器上写入的配置,会被持久化保存到Nacos自带的derby中,因此当我们重启Nacos之后,仍然可以看到之前的配置信息。

Nacos还支持将配置信息写入Mysql中:

  1. 在Mysql中,创建名为nacos的数据库 ;

  2. 在nacos数据库中,执行 nacos 安装目录的 conf 目录下的 mysql-schema.sql脚本, 创建表结构;

  3. 修改 Nacos安装目录 conf/application.properties文件

    spring.datasource.platform=mysql
    db.num=1
    # 这里的url要改成你自己的mysql数据库地址,并在你的mysql中创建名为nacos的数据库
    db.url.0=jdbc:mysql://11.162.196.16:3306/nacos?
    characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
    # 这里要改成你自己登录mysql的用户名和密码
    db.user.0=nacos_devtest
    db.password.0=youdontkno
    

监听多个配置文件

假设 bootstrap.yaml 有如下配置, 那么 Nacos 客户端启动时, 不仅会监听order-service-dev.yaml, 还会自动监听order-serviceorder-service.yaml两个文件;

spring:application:name: order-serviceprofiles:active: devcloud:nacos:config:server-addr: localhost:8848file-extension: yaml

如果要监听更多配置文件, 例如公共的 MySQL 配置文件, 例如公共的 Redis 配置文件, 进行如下配置

server:port: 7770
spring:application:name: order-serviceprofiles:active: devcloud:nacos:config:server-addr: localhost:8848file-extension: yamlshared-configs:- data-id: mysql.yamlrefresh: true- data-id: redis.yamlrefresh: true

在这里插入图片描述

发布配置原理2.0.4

客户端

Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "localhost");
ConfigService configService = NacosFactory.createConfigService(properties);
configService.publishConfig("test-mysql.yaml", "DEFAULT_GROUP", "my: content");
  1. 客户端通过 NacosConfigService 向 Nacos 配置中心发出 ConfigPublishRequest请求;
  2. 这个请求里封装了命名空间, Group, dataId, 配置内容等信息;
  3. 2.X 版本, 默认是使用的 gRPC 的方式发送;

服务端

以下分析基于开启了 MySQL 外置存储的情况;

服务端由 ConfigPublishRequestHandler 来处理发布请求;

  1. 将命名空间, 分组, dataId, 内容等信息从请求中提取出来, 然后插入到数据库中;

  2. 然后发布一个ConfigDataChangeEvent, 这个事件被异步地处理, 将配置的发布同步给 Nacos 集群中的其它结点;

  3. 然后再异步地将配置作为一个文件持久化保存到服务器的文件系统中;

    在这里插入图片描述

  4. 然后根据命名空间, 组名和data-id去获取内存中, 当前发布的配置集对应的缓存CacheItem, 这个缓存保存了配置集的一些描述信息, 例如由配置项计算出的MD5值, 例如上次修改时间, 例如配置文件的类型(比如yaml)等, 如果没有, 就新创建一个缓存;

    磁盘文件存内容, 内存缓存存描述信息;

    MD5 值作为配置的一种版本标记; 当一个配置文件的 MD5 改变, 认为该文件发生了更新;

  5. 检查缓存的 MD5 值(如果是新建的, MD5 = null) 与最新的配置集内容计算出的MD5值是否相同, 如果不同, 发布一个LocalDataChangeEvent , 推送新发布的配置给订阅了这个配置的客户端;

拉取配置原理

基于2.0.4版本

客户端

  1. ApplicationContext 初始化之前, 会有一个独立于应用程序主上下文的 BootstrapContext 先被创建并初始化, 这个上下文读取的是 bootstrap.yaml 文件;

  2. 这个上下文用于引导配置属性源;

  3. 了解: 在主上下文的 prepareEnvironment 阶段, 会发布一个EnvironmentPrepared 事件; 这个事件会被BootstrapApplicationListener( 也是在 spring-cloud- context.spring.facories文件中声明的 ) 处理;
    它会创建并初始化一个 BootstrapContext

  4. BootstrapContext 初始化时, 会加载 spring.factories 文件中声明的 BootstrapConfiguration类型的组件, 进行自动配置;

  5. SpringCloud 官方团队在spring-cloud-context-3.1.4.jar\META-INF\spring.factories文件中提供了一个 BoostrapConfiguration, 叫PropertySourceBootstrapConfiguration; 同时, 它也是一个初始化器 ApplicationContextInitializer;

  6. 这个初始化器会自动注入 BootstrapContext 中的所有 PropertySourceLocator, 包括NacosPropertySourceLocator

  7. NacosPropertySourceLocator是由 Nacos提供的 NacosConfigBootstrapConfiguration 注册的, 这个配置类也是一个BootstrapConfiguration;

  8. 这个已经注入NacosPropertySourceLocator的初始化器PropertySourceBootstrapConfiguration 会放到SpringApplication对象中保存;

  9. 这样, 就通过 SpringApplication 对象在 BootstrapContext 和应用的主上下文之间传递了PropertySourceBootstrapConfiguration

  10. 应用的主上下文在 prepareContext 阶段, 调用 applyInitializers 时, 会调用所有初始化器的初始化方法, 这其中就包括PropertySourceBootstrapConfiguration的初始化方法; 这个初始化方法会遍历自己持有的PropertySourceLocator, 并调用他们的 locateCollection 方法

    为什么采用初始化器ApplicationContextInitializerBootstrapConfiguration的方式去发送拉取配置的请求? 因为要确保在ApplicationContext初始化之前就能拿到 Nacos 配置中心中的配置, 这样才能在refreshContext的时候去使用这些配置

  11. 所以, Nacos 自动配置类注入的 NacosPropertySourceLocator 就派上用场了;

  12. NacosPropertySourceLocator中, 从 Environment 中获取到 bootstrap.yaml 文件对应的 PropertySource, 进而拿到 Nacos 配置中心的地址, 和要拉取的配置文件的 DataId 等信息,

  13. 创建NacosConfigService, 调用其getConfig方法拉取配置;

  14. getConfig 方法中, 会首先尝试从本地的一个特定路径下读取配置文件, 这个文件目录中间有一级是data开头的;

  15. 但是, 客户端在生成本地缓存的时候, 是放到的snapshot开头的文件夹下; 所以除非你手动创建data开头的本地缓存, 这里永远读不到数据;

    根据 Github Issues 中 Nacos 作者的官方回答, 这是一种容灾机制, 防止连不上 Nacos 配置中心, 应用程序无法启动;

    连不上的时候, 你就手动创建data 开头的缓存目录, 然后把 snapshot 文件夹下自动保存的配置文件复制进去;

  16. 如果从本地缓存读到了, 直接返回, 但一般都是读不到;

  17. 如果读不到, 发送拉取配置的请求, 从 Nacos 服务器获取配置集;

  18. 将从服务器获取到的配置集保存到 snapshot 文件夹下;

  19. 将获取到的配置集封装成属性源, 保存到 Environment 中; 以后就可以使用了; 每一个 dataId, 都会对应一个属性源 PropertySource;

  20. 并且, 来自 Nacos 配置中心的属性源, 会在 Environment 内的 PropertySources 内的 List 中排最前;

  21. 另外, 这些来自Nacos 的属性源, 还会在这时被添加到一个 NacosPropertySourceRepository 中, 后面订阅的时候就是从这里取的要订阅的属性源, 获取Group和 DataId (为什么没有命名空间? 因为一个客户端获取的配置集的命名空间是固定的, 保存在一个固定的地方, 发请求的时候当然也会携带上命名空间);

服务端

  1. ConfigQueryRequestHandler中处理拉取请求;

  2. 默认情况下, 读取服务器本地的缓存文件, 而不是数据库; 读取后返回给客户端; 如果本地缓存没有读到, 也不会去数据库中读, 返回空;

    可以进行配置, 让服务器从数据库读取, 而不是本地缓存;

  3. 在服务器启动的时候, 会从数据库拉取配置信息, 保存到服务器文件系统中;

默认情况下, 手动修改数据库中的配置集内容, 只会影响到控制台, 不会影响到Nacos本地缓存, 也就不会改变客户端拉取配置的结果;

只有Nacos Server 重启的时候, 才会将数据库中的内容装载到服务器本地;

控制台

控制台显示的配置信息是直接从数据库中读取的;

这里强烈建议大家自己动手验证, 手动修改数据库中保存的配置, 然后去看控制台的显示和客户端拉到的配置是否一致;

这里是一个大坑, 有时候明明控制台看到的是 variable = a, 但项目中读到的可能是 variable = b;

订阅配置

客户端发起订阅

  1. 和拉取配置不同, 你得先准备好了 Web 容器, 才能去订阅配置, 所以应用启动时自动订阅配置, 是由 Nacos-Config 的普通自动配置类向 Spring 容器中注册了一个 ApplicationListener, 叫NacosContextRefresher;
  2. 由这个监听器, 监听 ApplicationReady 事件 ( 这个事件是在 callRunners 之后发布的 );
  3. 监听到之后, 就会遍历 NacosPropertySourceRepository, 获取每个 Nacos 属性源的组名和DataId, 然后注册与之对应的监听器:
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,// 回调函数                                        lst -> new AbstractSharedListener() {@Overridepublic void innerReceive(String dataId, String group,String configInfo) {refreshCountIncrement();nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));if (log.isDebugEnabled()) {log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));}}});
try {// 向服务器注册监听器;configService.addListener(dataKey, groupKey, listener);log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,groupKey);
}

服务器处理监听请求

  1. 抽取出客户端的 ConnectionId, 和要监听的文件的标识 groupKey, 虽然叫groupKey, 但实际是DataId-Group-namespace构成的;

在这里插入图片描述

  1. ConfigChangeListenContext中维护订阅关系;
public class ConfigChangeListenContext {/*** <GroupKey, Set<监听了这个GroupKey的ConnnectId>>*/private ConcurrentHashMap<String, HashSet<String>> groupKeyContext = new ConcurrentHashMap<String, HashSet<String>>();/*** <客户端ID, Map<这个客户端监听的GroupKey, 配置的MD5>值*/private ConcurrentHashMap<String, HashMap<String, String>> connectionIdContext = new ConcurrentHashMap<String, HashMap<String, String>>();   
}

服务器推送更新

在这里插入图片描述

  • 这个事件被RpcConfigChangeNotifier监听器处理, 从事件中提取出对应的配置集的 DataId, group, groupKey 等等信息;
  • 从维护监听关系的对象ConfigChangeListenContext中, 根据 GroupKey取出监听了这个配置集的所有客户端的ConnectionId;
  • 遍历所有 ConnectionId, 将新的配置, 封装成一个配置变化的通知请求, 通过 RPC 工具将请求 Push 给客户端;

客户端热更新

使用 Java API 监听某个来自 Nacos 的配置文件, 配置文件重新发布后, Environment 中的 PropertySource 确实更新了, 但是使用 @Value 注解注入了某个属性值的Bean, 他的属性值没有改变; 因为 @Value 通过反射将属性值注入后, 这个属性值就保存在 Bean 中了, 修改 Environment 不会影响到它;

Nacos 采取的策略是将配置了热更新 ( @RefreshScope ) 的 Bean 销毁, 重新创建;

客户端收到配置变化的通知请求后, 发布一个刷新事件

String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,// 回调函数                                        lst -> new AbstractSharedListener() {@Overridepublic void innerReceive(String dataId, String group,String configInfo) {refreshCountIncrement();nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));if (log.isDebugEnabled()) {log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));}}});
try {// 向服务器注册监听器;configService.addListener(dataKey, groupKey, listener);log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,groupKey);
}

这个刷新事件最终来到ContextRefresher进行处理, 做了两个重要操作

一是更新 Environment 里面对应的 PropertySource, 更新完以后会发布一个EnvironmentChangeEvent, 是一个扩展点; 例如有一个监听器监听到这个事件后, 会更新 @ConfigurationProperties 注解修饰的参数类;

二是把有@RefreshScope注解修饰的 Bean 销毁重建; 完成后发布一个RefreshScopeRefreshedEvent, Gateway 就是通过对这个事件的监听, 完成路由配置的热更新;

重建bean
  1. @RefreshScope注解的本质是@Scope("refresh"); 一个 Bean 的 Scope 可以是 singleton, 可以是 prototype, 可以是 Session (每个 Session 创建一个新的 Bean); 在创建 bean的时候, 会根据 BeanDefinition 中的 Scope 进行不同的创建逻辑;
  2. 总得来说: 所有 Scope 为 refresh 的 Bean, 创建时都会被放到同一个缓存中;
  3. 更新 Environment 后, 发布一个环境更新事件, 由ConfigurationPropertiesRebinder处理这个事件;
  4. 销毁 refreshScope 缓存中的 bean, 然后重新初始化这些被销毁的 bean;
安全问题
  1. 销毁时加锁, 防止多个线程同时销毁;
  2. 销毁之前, 创建一个代理, 在新的 bean 没有准备好之前, 用这个代理;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/394154.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Milvus与Zilliz Cloud:向量数据库高可用性的双重飞跃

向量数据库高可用性的重要性及其在现代数据分析中的关键作用 在数据爆炸式增长的今天,企业对于高效、准确地处理和分析大规模数据集的需求日益迫切。尤其是在人工智能、机器学习、图像识别、自然语言处理等领域,向量数据库因其对高维数据的高效存储与检索能力,成为了不可或…

Elasticsearch未授权访问漏洞

步骤一:使用以下Fofa语法进行Elasticsearch产品搜索.. fofa语法"Elasticsearch" && port"9200" 步骤二:存在未授权访问则直接进入到信息页面...不需要输入用户密码登陆. http://localhost:9200/_plugin/head/web管理界面 http://localhost:9200/…

【JavaEE】线程池

目录 前言 什么是线程池 线程池的优点 ThreadPollExecutor中的构造方法 corePoolSize && maximumPoolSize keepAliveTime && unit workQueue threadFactory 如何在java中使用线程池 1.创建线程池对象 2.调用submit添加任务 3.调用shutdown关闭线程池…

【Python】requests的response.text 和 urllib.request 的 response.read()的区别

刚写代码的时候&#xff0c;我经常会把requests 和 urllib下的request 包搞混&#xff0c;这两个请求响应的方法看起来很相似&#xff0c;但是写获取的方法是不一样的。 前者requests 是用response.text 来获取源码&#xff0c;而 urllib.request是用 response.read() 来获取h…

数学建模--智能算法之免疫算法

目录 基本原理 应用实例 代码示例 总结 免疫算法在免疫系统研究中的应用和进展是什么&#xff1f; 如何量化评估免疫算法在不同优化问题中的性能和效率&#xff1f; 免疫算法与其他智能优化算法&#xff08;如遗传算法、粒子群优化&#xff09;相比有哪些独特优势和局限性…

【C++】C++11的新特性 — 线程库 ,原子操作 , 条件变量

勇敢就是接受发生在你身上的事&#xff0c;并把它尽力做到最好。 -- 约翰・欧文 -- C11的新特性 1 线程1.1 线程概念1.2 C中的线程1.3 线程并行1.4 锁 2 原子操作3 条件变量Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读&#xff01;&#xff01;&#xff01;下…

编译和汇编的区别

一、编译 编译是将高级语言&#xff08;如C、C、Java等&#xff09;编写的源代码转换成计算机可以直接执行的低级语言&#xff08;通常是机器语言或汇编语言&#xff09;的过程 编译 —— 将人类可读的源代码转换为计算机可执行的指令集 编译过程 通常包括词法分析、语法分…

正点原子imx6ull-mini-Linux驱动之Linux 网络驱动实验

网络驱动是 linux 里面驱动三巨头之一&#xff0c;linux 下的网络功能非常强大&#xff0c;嵌入式 linux 中也常 常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动&#xff0c;本章我们就来学习一下 linux 里面的网络设备驱动。 1&#xff1a;嵌入式网络简介 1.1…

如何给源代码加密?这款加密软件教给你操作步骤

给源代码加密是保护软件知识产权和商业机密的重要手段。以这款加密软件安企神为例&#xff0c;给源代码加密的过程可以概述为以下几个方面。 一、安企神软件概述 安企神是一款功能强大的企业安全加密软件&#xff0c;它提供了全面的源代码防泄漏解决方案。该软件通过透明文件加…

扩散模型系列笔记(一)——DDPM

直观理解 扩散模型分为前向过程&#xff08;扩散过程&#xff0c;Data → \to →Noise&#xff09;和后向过程&#xff08;生成过程或逆扩散过程&#xff0c;Noise → \to →Data&#xff09;。在前向过程中&#xff0c;对于每一个观测样本&#xff0c;不断向样本中添加少量噪…

Leetcode每日刷题之字符串中的第一个唯一字符(C++)

在学习的过程中对代码的熟练运用至关重要&#xff0c;练习解决实际问题就可以很好的锻炼自己的编程能力&#xff0c;接下来让我们练习这道 387.字符串中的第一个唯一字符 思路解析 根据题意我们可以知道这个字符串只有小写字母&#xff0c;并且可能包含多个唯一字符&#xff0…

Java---字符串string练习

目录&#xff1a; 1.将数字转换成罗马数字 2.键盘输入任意字符串&#xff0c;打乱里面的内容 3.返回字符串中最后一个单词长度 4.调整A字符串 看是否可与B字符串匹配 一&#xff1a; //键盘录入一个字符串// 长度小于等于9 只能是数字// -将内容变成罗马数字// Ⅰ Ⅱ Ⅲ Ⅳ…

智慧水务项目(二)django(drf)+angular 18 创建通用model,并对orm常用字段进行说明

一、说明 上一篇文章建立一个最简单的项目&#xff0c;现在我们建立一个公共模型&#xff0c;抽取公共字段&#xff0c;以便于后续模块继承&#xff0c;过程之中会对orm常用字段进行说明&#xff0c;用到的介绍一下 二、创建一个db.py 目录如下图 1、代码 from importlib im…

基于QT实现的简易WPS(已开源)

一、开发工具及开源地址&#xff1a; 开发工具&#xff1a;QTCreator &#xff0c;QT 5 开源地址&#xff1a; GitHub - Whale-xh/WPS_official: Simple WPS based on QTSimple WPS based on QT. Contribute to Whale-xh/WPS_official development by creating an acc…

推荐 3个实用且完全免费的在线工具,每天都会用到,无需登录打开即用

100font 100font是一个专业的免费商用字体下载网站&#xff0c;专注于收集、整理和分享各种免费无版权的商用字体。用户可以在这个平台上找到并下载简体中文、繁体中文、英文、日文、韩文等多种语言类型的字体。 该网站的特点包括清晰的分类和直观的下载流程&#xff0c;用户可…

进阶SpringBoot之 Spring 官网或 IDEA 快速构建项目

SpringBoot 就是一个 JavaWeb 的开发框架&#xff0c;约定大于配置 程序 数据结构 算法 微服务架构是把每个功能元素独立出来&#xff0c;再动态组合&#xff0c;是对功能元素的复制 这样做可以节省调用资源&#xff0c;每个功能元素的服务都是一个可替代、可独立升级的软…

算法混合杂项

基础类型 可用template 投影 是有方向的 求俩直线交点 推公式 q我们不知道&#xff0c;已知p1 p2&#xff0c;正弦定理&#xff0c;α可以用叉积表示出来 β同理 所以我们能求出p1q 已知piq 回归到我们上一个问题&#xff0c;已知方向和长度&#xff0c;我们就能够求出Voq …

C语言 ——— 学习并使用字符分类函数

目录 学习isupper函数 学习isdigit函数 学习tolower函数 将输入的字符串中把大写字母转换为小写字母并输出 学习isupper函数 参数部分&#xff1a; 形参需要传递的是一个字母&#xff0c;字符在ASCII码表上是以整型存储的&#xff0c;所以实参部分用(int c)没有问题 返回…

【iOS】AutoreleasePool自动释放池的实现原理

目录 ARC与MRC项目中的main函数自动释放池autoreleasepool {}实现原理AutoreleasePoolPage总结 objc_autoreleasePoolPush的源码分析autoreleaseNewPageautoreleaseFullPageautoreleaseNoPage autoreleaseFast总结 autorelease方法源码分析objc_autoreleasePoolPop的源码分析po…

谁来做引领企业精益变革的舵手最合适?

在这个瞬息万变的商业时代&#xff0c;企业如同航行在波涛汹涌的大海中的巨轮&#xff0c;既需面对未知的挑战&#xff0c;也要抓住稍纵即逝的机遇。而在这场没有终点的航行中&#xff0c;引领企业实现精益变革的舵手&#xff0c;无疑是推动企业破浪前行、稳健致远的关键角色。…